14 Commits
0.1.4 ... 0.1.8

Author SHA1 Message Date
2e1bc4840b 1.8 bump
Some checks failed
Create Release / build (push) Has been cancelled
2025-08-25 18:33:08 +01:00
8526f6c240 Cosmetic "fix" 2025-08-25 18:25:41 +01:00
77d62b68b4 PenumbraAPI 5.12 fixes 2025-08-25 18:21:14 +01:00
bc72660c7f Removed RepoChange service. All configuration is now done locally without a web ping first.
Some checks failed
Create Release / build (push) Has been cancelled
2025-08-23 22:52:52 +01:00
8a27634d06 Fixed repo changer. 2025-08-23 20:02:19 +01:00
d7355c5eb9 Fixes 2025-08-23 19:56:21 +01:00
46558bf4f9 Full removal of Lop's phone home code. 2025-08-23 19:52:46 +01:00
e5dd13bc3e Bluer blue. 2025-08-23 18:04:35 +01:00
a1a3858653 Version bump: 0.1.6
Some checks failed
Create Release / build (push) Has been cancelled
2025-08-23 17:53:09 +01:00
deea0e90b1 Blue progress bar 2025-08-23 17:51:05 +01:00
d8b306148f Set autopause thresholds. Now defaults to automatically on. 500MB VRAM and 175k tris. 2025-08-23 17:45:53 +01:00
702b91b87a Version bump, I'm dumb.
Some checks failed
Create Release / build (push) Has been cancelled
2025-08-23 04:16:06 +01:00
084cfb518a Version bump, I'm dumb. 2025-08-23 04:15:51 +01:00
fa2d647a64 TOS updates to clarify account deletion 2025-08-23 04:13:51 +01:00
11 changed files with 45 additions and 565 deletions

View File

@@ -225,9 +225,15 @@ public sealed class IpcCallerPenumbra : DisposableMediatorSubscriberBase, IIpcCa
return await _dalamudUtil.RunOnFrameworkThread(() =>
{
Guid collId;
var collName = "ElfSync_" + uid;
var collId = _penumbraCreateNamedTemporaryCollection.Invoke(collName);
PenumbraApiEc penEC = _penumbraCreateNamedTemporaryCollection.Invoke(uid, collName, out collId);
logger.LogTrace("Creating Temp Collection {collName}, GUID: {collId}", collName, collId);
if (penEC != PenumbraApiEc.Success)
{
logger.LogError("Failed to create temporary collection for {collName} with error code {penEC}. Please include this line in any error reports", collName, penEC);
return Guid.Empty;
}
return collId;
}).ConfigureAwait(false);

View File

@@ -5,11 +5,11 @@ namespace MareSynchronos.MareConfiguration.Configurations;
public class PlayerPerformanceConfig : IMareConfiguration
{
public int Version { get; set; } = 1;
public bool AutoPausePlayersExceedingThresholds { get; set; } = false;
public bool AutoPausePlayersExceedingThresholds { get; set; } = true;
public bool NotifyAutoPauseDirectPairs { get; set; } = true;
public bool NotifyAutoPauseGroupPairs { get; set; } = false;
public int VRAMSizeAutoPauseThresholdMiB { get; set; } = 550;
public int TrisAutoPauseThresholdThousands { get; set; } = 375;
public int VRAMSizeAutoPauseThresholdMiB { get; set; } = 500;
public int TrisAutoPauseThresholdThousands { get; set; } = 175;
public bool IgnoreDirectPairs { get; set; } = true;
public TextureShrinkMode TextureShrinkMode { get; set; } = TextureShrinkMode.Default;
public bool TextureShrinkDeleteOriginal { get; set; } = false;

View File

@@ -2,7 +2,7 @@
<Project Sdk="Dalamud.NET.Sdk/13.0.0">
<PropertyGroup>
<AssemblyName>Snowcloak</AssemblyName>
<Version>0.1.3</Version>
<Version>0.1.8</Version>
<PackageProjectUrl>https://github.com/Eauldane/SnowcloakClient/</PackageProjectUrl>
</PropertyGroup>
@@ -36,7 +36,7 @@
</ItemGroup>
<ItemGroup Condition="!Exists('.\Penumbra.Api\Penumbra.Api.csproj')">
<PackageReference Include="Penumbra.Api" Version="5.10.0" />
<PackageReference Include="Penumbra.Api" Version="5.12.0" />
</ItemGroup>
<ItemGroup Condition="Exists('.\Glamourer.Api\Glamourer.Api.csproj')">

View File

@@ -86,8 +86,8 @@ public class Pair : DisposableMediatorSubscriberBase
{
Name = name,
OnClicked = action,
PrefixColor = 559,
PrefixChar = 'L'
PrefixColor = 304, //TODO: Figure out the colour table
PrefixChar = 'S'
});
}

View File

@@ -126,7 +126,6 @@ public sealed class Plugin : IDalamudPlugin
collection.AddSingleton<BlockedCharacterHandler>();
collection.AddSingleton<IpcProvider>();
collection.AddSingleton<VisibilityService>();
collection.AddSingleton<RepoChangeService>();
collection.AddSingleton<EventAggregator>();
collection.AddSingleton<DalamudUtilService>();
collection.AddSingleton<DtrEntry>();
@@ -210,7 +209,6 @@ public sealed class Plugin : IDalamudPlugin
collection.AddHostedService(p => p.GetRequiredService<EventAggregator>());
collection.AddHostedService(p => p.GetRequiredService<MarePlugin>());
collection.AddHostedService(p => p.GetRequiredService<IpcProvider>());
collection.AddHostedService(p => p.GetRequiredService<RepoChangeService>());
collection.AddHostedService(p => p.GetRequiredService<NoSnapService>());
})
.Build();

View File

@@ -56,112 +56,9 @@ public sealed class RemoteConfigurationService
private async Task DownloadConfig()
{
string? jsonResponse = null;
// Removed Lop's remote config code. Function exists purely to keep things clean.
LoadConfig();
foreach (var remoteUrl in ConfigSources)
{
try
{
_logger.LogDebug("Fetching {url}", remoteUrl);
using var httpClient = new HttpClient(
new HttpClientHandler
{
AllowAutoRedirect = true,
MaxAutomaticRedirections = 5
}
);
httpClient.Timeout = TimeSpan.FromSeconds(6);
var ver = Assembly.GetExecutingAssembly().GetName().Version;
httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("MareSynchronos", ver!.Major + "." + ver!.Minor + "." + ver!.Build));
var request = new HttpRequestMessage(HttpMethod.Get, remoteUrl);
if (remoteUrl.Equals(_configService.Current.Origin, StringComparison.Ordinal))
{
if (!string.IsNullOrEmpty(_configService.Current.ETag))
request.Headers.IfNoneMatch.Add(new EntityTagHeaderValue(_configService.Current.ETag));
if (_configService.Current.LastModified != null)
request.Headers.IfModifiedSince = _configService.Current.LastModified;
}
var response = await httpClient.SendAsync(request).ConfigureAwait(false);
if (response.StatusCode == HttpStatusCode.NotModified)
{
_logger.LogDebug("Using cached remote configuration from {url}", remoteUrl);
return;
}
response.EnsureSuccessStatusCode();
var contentType = response.Content.Headers.ContentType?.MediaType;
if (contentType == null || !contentType.Equals("application/json", StringComparison.Ordinal))
{
_logger.LogWarning("HTTP request for remote config failed: wrong MIME type");
continue;
}
_logger.LogInformation("Downloaded new configuration from {url}", remoteUrl);
_configService.Current.Origin = remoteUrl;
_configService.Current.ETag = response.Headers.ETag?.ToString() ?? string.Empty;
try
{
if (response.Content.Headers.Contains("Last-Modified"))
{
var lastModified = response.Content.Headers.GetValues("Last-Modified").First();
_configService.Current.LastModified = DateTimeOffset.Parse(lastModified, System.Globalization.CultureInfo.InvariantCulture);
}
}
catch
{
_configService.Current.LastModified = null;
}
jsonResponse = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
break;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "HTTP request for remote config failed");
if (remoteUrl.Equals(_configService.Current.Origin, StringComparison.Ordinal))
{
_configService.Current.ETag = string.Empty;
_configService.Current.LastModified = null;
_configService.Save();
}
}
}
if (jsonResponse == null)
{
_logger.LogWarning("Could not download remote config");
return;
}
try
{
var jsonDoc = JsonNode.Parse(jsonResponse) as JsonObject;
if (jsonDoc == null)
{
_logger.LogWarning("Downloaded remote config is not a JSON object");
return;
}
LoadConfig(jsonDoc);
}
catch (JsonException ex)
{
_logger.LogWarning(ex, "Invalid JSON in remote config response");
}
}
private static bool VerifySignature(string message, ulong ts, string signature, string pubKey)
@@ -172,29 +69,12 @@ public sealed class RemoteConfigurationService
return Ed25519.Verify(sig, msg, pub);
}
private void LoadConfig(JsonObject jsonDoc)
private void LoadConfig()
{
ulong ts = 1755859494;
//if (ts <= _configService.Current.Timestamp)
//{
// _logger.LogDebug("Remote configuration is not newer than cached config");
// return;
//}
var configString = "{\"mainServer\":{\"api_url\":\"wss://hub.snowcloak-sync.com/\",\"hub_url\":\"wss://hub.snowcloak-sync.com/mare\"},\"noSnap\":{\"listOfPlugins\":[\"Snapper\",\"Snappy\",\"Meddle.Plugin\"]}}";
var signatures = jsonDoc["sig"]!.AsObject();
var configString = "{\"mainServer\":{\"api_url\":\"wss://hub.snowcloak-sync.com/\",\"hub_url\":\"wss://hub.snowcloak-sync.com/mare\"},\"repoChange\":{\"current_repo\":\"https://hub.snowcloak-sync.com/repo.json\",\"valid_repos\":[\"https://hub.snowcloak-sync.com/repo.json\"]},\"noSnap\":{\"listOfPlugins\":[\"Snapper\",\"Snappy\",\"Meddle.Plugin\"]}}";
// var configString = jsonDoc["config"]!.GetValue<string>();
// bool verified = signatures.Any(sig =>
// ConfigPublicKeys.TryGetValue(sig.Key, out var pubKey) &&
// VerifySignature(configString, ts, sig.Value!.GetValue<string>(), pubKey));
//bool verified = true;
// if (!verified)
// {
// _logger.LogWarning("Could not verify signature for downloaded remote config");
// return;
// }
_configService.Current.Configuration = JsonNode.Parse(configString)!.AsObject();
_configService.Current.Timestamp = ts;

View File

@@ -1,12 +0,0 @@
using System.Text.Json.Serialization;
namespace MareSynchronos.Services;
public record RepoChangeConfig
{
[JsonPropertyName("current_repo")]
public string? CurrentRepo { get; set; }
[JsonPropertyName("valid_repos")]
public string[]? ValidRepos { get; set; }
}

View File

@@ -1,401 +0,0 @@
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Reflection;
namespace MareSynchronos.Services;
/* Reflection code based almost entirely on ECommons DalamudReflector
MIT License
Copyright (c) 2023 NightmareXIV
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
public sealed class RepoChangeService : IHostedService
{
#region Reflection Helpers
private const BindingFlags AllFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static;
private const BindingFlags StaticFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static;
private const BindingFlags InstanceFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
private static object GetFoP(object obj, string name)
{
Type? type = obj.GetType();
while (type != null)
{
var fieldInfo = type.GetField(name, AllFlags);
if (fieldInfo != null)
{
return fieldInfo.GetValue(obj)!;
}
var propertyInfo = type.GetProperty(name, AllFlags);
if (propertyInfo != null)
{
return propertyInfo.GetValue(obj)!;
}
type = type.BaseType;
}
throw new Exception($"Reflection GetFoP failed (not found: {obj.GetType().Name}.{name})");
}
private static T GetFoP<T>(object obj, string name)
{
return (T)GetFoP(obj, name);
}
private static void SetFoP(object obj, string name, object value)
{
var type = obj.GetType();
var field = type.GetField(name, AllFlags);
if (field != null)
{
field.SetValue(obj, value);
}
else
{
var prop = type.GetProperty(name, AllFlags)!;
if (prop == null)
throw new Exception($"Reflection SetFoP failed (not found: {type.Name}.{name})");
prop.SetValue(obj, value);
}
}
private static object? Call(object obj, string name, object[] @params, bool matchExactArgumentTypes = false)
{
MethodInfo? info;
var type = obj.GetType();
if (!matchExactArgumentTypes)
{
info = type.GetMethod(name, AllFlags);
}
else
{
info = type.GetMethod(name, AllFlags, @params.Select(x => x.GetType()).ToArray());
}
if (info == null)
throw new Exception($"Reflection Call failed (not found: {type.Name}.{name})");
return info.Invoke(obj, @params);
}
private static T Call<T>(object obj, string name, object[] @params, bool matchExactArgumentTypes = false)
{
return (T)Call(obj, name, @params, matchExactArgumentTypes)!;
}
#endregion
#region Dalamud Reflection
public object GetService(string serviceFullName)
{
return _pluginInterface.GetType().Assembly.
GetType("Dalamud.Service`1", true)!.MakeGenericType(_pluginInterface.GetType().Assembly.GetType(serviceFullName, true)!).
GetMethod("Get")!.Invoke(null, BindingFlags.Default, null, Array.Empty<object>(), null)!;
}
private object GetPluginManager()
{
return _pluginInterface.GetType().Assembly.
GetType("Dalamud.Service`1", true)!.MakeGenericType(_pluginInterface.GetType().Assembly.GetType("Dalamud.Plugin.Internal.PluginManager", true)!).
GetMethod("Get")!.Invoke(null, BindingFlags.Default, null, Array.Empty<object>(), null)!;
}
private void ReloadPluginMasters()
{
var mgr = GetService("Dalamud.Plugin.Internal.PluginManager");
var pluginReload = mgr.GetType().GetMethod("SetPluginReposFromConfigAsync", BindingFlags.Instance | BindingFlags.Public)!;
pluginReload.Invoke(mgr, [true]);
}
public void SaveDalamudConfig()
{
var conf = GetService("Dalamud.Configuration.Internal.DalamudConfiguration");
var configSave = conf?.GetType().GetMethod("QueueSave", BindingFlags.Instance | BindingFlags.Public);
configSave?.Invoke(conf, null);
}
private IEnumerable<object> GetRepoByURL(string repoURL)
{
var conf = GetService("Dalamud.Configuration.Internal.DalamudConfiguration");
var repolist = (System.Collections.IEnumerable)GetFoP(conf, "ThirdRepoList");
foreach (var r in repolist)
{
if (((string)GetFoP(r, "Url")).Equals(repoURL, StringComparison.OrdinalIgnoreCase))
yield return r;
}
}
private bool HasRepo(string repoURL)
{
var conf = GetService("Dalamud.Configuration.Internal.DalamudConfiguration");
var repolist = (System.Collections.IEnumerable)GetFoP(conf, "ThirdRepoList");
foreach (var r in repolist)
{
if (((string)GetFoP(r, "Url")).Equals(repoURL, StringComparison.OrdinalIgnoreCase))
return true;
}
return false;
}
private void AddRepo(string repoURL, bool enabled)
{
var conf = GetService("Dalamud.Configuration.Internal.DalamudConfiguration");
var repolist = (System.Collections.IEnumerable)GetFoP(conf, "ThirdRepoList");
foreach (var r in repolist)
{
if (((string)GetFoP(r, "Url")).Equals(repoURL, StringComparison.OrdinalIgnoreCase))
return;
}
var instance = Activator.CreateInstance(_pluginInterface.GetType().Assembly.GetType("Dalamud.Configuration.ThirdPartyRepoSettings")!)!;
SetFoP(instance, "Url", repoURL);
SetFoP(instance, "IsEnabled", enabled);
GetFoP<System.Collections.IList>(conf, "ThirdRepoList").Add(instance!);
}
private void RemoveRepo(string repoURL)
{
var toRemove = new List<object>();
var conf = GetService("Dalamud.Configuration.Internal.DalamudConfiguration");
var repolist = (System.Collections.IList)GetFoP(conf, "ThirdRepoList");
foreach (var r in repolist)
{
if (((string)GetFoP(r, "Url")).Equals(repoURL, StringComparison.OrdinalIgnoreCase))
toRemove.Add(r);
}
foreach (var r in toRemove)
repolist.Remove(r);
}
public List<(object LocalPlugin, string InstalledFromUrl)> GetLocalPluginsByName(string internalName)
{
List<(object LocalPlugin, string RepoURL)> result = [];
var pluginManager = GetPluginManager();
var installedPlugins = (System.Collections.IList)pluginManager.GetType().GetProperty("InstalledPlugins")!.GetValue(pluginManager)!;
foreach (var plugin in installedPlugins)
{
if (((string)plugin.GetType().GetProperty("InternalName")!.GetValue(plugin)!).Equals(internalName, StringComparison.Ordinal))
{
var type = plugin.GetType();
if (type.Name.Equals("LocalDevPlugin", StringComparison.Ordinal))
continue;
var manifest = GetFoP(plugin, "manifest");
string installedFromUrl = (string)GetFoP(manifest, "InstalledFromUrl");
result.Add((plugin, installedFromUrl));
}
}
return result;
}
#endregion
private readonly ILogger<RepoChangeService> _logger;
private readonly RemoteConfigurationService _remoteConfig;
private readonly IDalamudPluginInterface _pluginInterface;
private readonly IFramework _framework;
public RepoChangeService(ILogger<RepoChangeService> logger, RemoteConfigurationService remoteConfig, IDalamudPluginInterface pluginInterface, IFramework framework)
{
_logger = logger;
_remoteConfig = remoteConfig;
_pluginInterface = pluginInterface;
_framework = framework;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogDebug("Starting RepoChange Service");
var repoChangeConfig = await _remoteConfig.GetConfigAsync<RepoChangeConfig>("repoChange").ConfigureAwait(false) ?? new();
var currentRepo = repoChangeConfig.CurrentRepo;
var validRepos = (repoChangeConfig.ValidRepos ?? []).ToList();
if (!currentRepo.IsNullOrEmpty() && !validRepos.Contains(currentRepo, StringComparer.Ordinal))
validRepos.Add(currentRepo);
if (validRepos.Count == 0)
{
_logger.LogInformation("No valid repos configured, skipping");
return;
}
await _framework.RunOnTick(() =>
{
try
{
var internalName = Assembly.GetExecutingAssembly().GetName().Name!;
var localPlugins = GetLocalPluginsByName(internalName);
var suffix = string.Empty;
if (localPlugins.Count == 0)
{
_logger.LogInformation("Skipping: No intalled plugin found");
return;
}
var hasValidCustomRepoUrl = false;
foreach (var vr in validRepos)
{
var vrCN = vr.Replace(".json", "_CN.json", StringComparison.Ordinal);
var vrKR = vr.Replace(".json", "_KR.json", StringComparison.Ordinal);
if (HasRepo(vr) || HasRepo(vrCN) || HasRepo(vrKR))
{
hasValidCustomRepoUrl = true;
break;
}
}
List<string> oldRepos = [];
var pluginRepoUrl = localPlugins[0].InstalledFromUrl;
if (pluginRepoUrl.Contains("_CN.json", StringComparison.Ordinal))
suffix = "_CN";
else if (pluginRepoUrl.Contains("_KR.json", StringComparison.Ordinal))
suffix = "_KR";
bool hasOldPluginRepoUrl = false;
foreach (var plugin in localPlugins)
{
foreach (var vr in validRepos)
{
var validRepo = vr.Replace(".json", $"{suffix}.json");
if (!plugin.InstalledFromUrl.Equals(validRepo, StringComparison.Ordinal))
{
oldRepos.Add(plugin.InstalledFromUrl);
hasOldPluginRepoUrl = true;
}
}
}
if (hasValidCustomRepoUrl)
{
if (hasOldPluginRepoUrl)
_logger.LogInformation("Result: Repo URL is up to date, but plugin install source is incorrect");
else
_logger.LogInformation("Result: Repo URL is up to date");
}
else
{
_logger.LogInformation("Result: Repo URL needs to be replaced");
}
if (currentRepo.IsNullOrEmpty())
{
_logger.LogWarning("No current repo URL configured");
return;
}
// Pre-test plugin repo url rewriting to ensure it succeeds before replacing the custom repo URL
if (hasOldPluginRepoUrl)
{
foreach (var plugin in localPlugins)
{
var manifest = GetFoP(plugin.LocalPlugin, "manifest");
if (manifest == null)
throw new Exception("Plugin manifest is null");
var manifestFile = GetFoP(plugin.LocalPlugin, "manifestFile");
if (manifestFile == null)
throw new Exception("Plugin manifestFile is null");
var repo = GetFoP(manifest, "InstalledFromUrl");
if (((string)repo).IsNullOrEmpty())
throw new Exception("Plugin repo url is null or empty");
SetFoP(manifest, "InstalledFromUrl", repo);
}
}
if (!hasValidCustomRepoUrl)
{
try
{
foreach (var oldRepo in oldRepos)
{
_logger.LogInformation("* Removing old repo: {r}", oldRepo);
RemoveRepo(oldRepo);
}
}
finally
{
_logger.LogInformation("* Adding current repo: {r}", currentRepo);
AddRepo(currentRepo, true);
}
}
// This time do it for real, and crash the game if we fail, to avoid saving a broken state
if (hasOldPluginRepoUrl)
{
try
{
_logger.LogInformation("* Updating plugins");
foreach (var plugin in localPlugins)
{
var manifest = GetFoP(plugin.LocalPlugin, "manifest");
if (manifest == null)
throw new Exception("Plugin manifest is null");
var manifestFile = GetFoP(plugin.LocalPlugin, "manifestFile");
if (manifestFile == null)
throw new Exception("Plugin manifestFile is null");
var repo = GetFoP(manifest, "InstalledFromUrl");
if (((string)repo).IsNullOrEmpty())
throw new Exception("Plugin repo url is null or empty");
SetFoP(manifest, "InstalledFromUrl", currentRepo);
Call(manifest, "Save", [manifestFile, "RepoChange"]);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception while changing plugin install repo");
foreach (var oldRepo in oldRepos)
{
_logger.LogInformation("* Restoring old repo: {r}", oldRepo);
AddRepo(oldRepo, true);
}
}
}
if (!hasValidCustomRepoUrl || hasOldPluginRepoUrl)
{
_logger.LogInformation("* Saving dalamud config");
SaveDalamudConfig();
_logger.LogInformation("* Reloading plugin masters");
ReloadPluginMasters();
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception in RepoChangeService");
}
}, default, 10, cancellationToken).ConfigureAwait(false);
_logger.LogInformation("Started RepoChangeService");
}
public Task StopAsync(CancellationToken cancellationToken)
{
_ = cancellationToken;
_logger.LogDebug("Stopping RepoChange Service");
return Task.CompletedTask;
}
}

View File

@@ -163,13 +163,13 @@ public class DownloadUi : WindowMediatorSubscriberBase
UiSharedService.Color(0, 0, 0, transparency), 1);
drawList.AddRectFilled(dlBarStart with { X = dlBarStart.X - dlBarBorder, Y = dlBarStart.Y - dlBarBorder },
dlBarEnd with { X = dlBarEnd.X + dlBarBorder, Y = dlBarEnd.Y + dlBarBorder },
UiSharedService.Color(220, 220, 220, transparency), 1);
UiSharedService.Color(220, 220, 255, transparency), 1);
drawList.AddRectFilled(dlBarStart, dlBarEnd,
UiSharedService.Color(0, 0, 0, transparency), 1);
var dlProgressPercent = transferredBytes / (double)totalBytes;
drawList.AddRectFilled(dlBarStart,
dlBarEnd with { X = dlBarStart.X + (float)(dlProgressPercent * dlBarWidth) },
UiSharedService.Color(50, 205, 50, transparency), 1);
UiSharedService.Color(100, 100, 255, transparency), 1);
if (_configService.Current.TransferBarsShowText)
{

View File

@@ -167,6 +167,15 @@ The plugin creator tried their best to keep you secure. However, there is no gua
UiSharedService.TextWrapped("""
Mod files that are saved on the service will remain on the service as long as there are requests for the files from clients. After a period of not being used, the mod files will be automatically deleted.
""");
UiSharedService.TextWrapped("""
Accounts that are inactive for ninety (90) days will be deleted for privacy reasons.
""");
UiSharedService.TextWrapped("""
Snowcloak is operated from servers located in the European Union. You agree not to upload any content to the service that violates EU law; and more specifically, German law.
""");
UiSharedService.TextWrapped("""
You may delete your account at any time from within the Settings panel of the plugin. Any mods unique to you will then be removed from the server within 14 days.
""");
UiSharedService.TextWrapped("""
This service is provided as-is.
""");

View File

@@ -113,9 +113,9 @@
},
"Penumbra.Api": {
"type": "Direct",
"requested": "[5.10.0, )",
"resolved": "5.10.0",
"contentHash": "ZRIIXQCluNlLoI4I4gNYuYXqb7p48OR1P2/LcL27owToR/A/IG0mH8Ks2HguB/Kjq4RpCinaVkyufpGxGspbTA=="
"requested": "[5.12.0, )",
"resolved": "5.12.0",
"contentHash": "XGWviAZgokj2djpH50FWgM24jOTpKUuDHvd0HwrzBRY6BEMmpb3HfGIl8+BDE/DqbpH63u6aO2TvzUV6BmXT5w=="
},
"SonarAnalyzer.CSharp": {
"type": "Direct",