mirror of
https://github.com/JustArchiNET/ArchiSteamFarm.git
synced 2025-12-20 00:08:38 +00:00
SteamUserPermissions revolution
This is needed for defining multiple operators and/or masters, as well as eventual further enhancements
This commit is contained in:
@@ -62,7 +62,7 @@ namespace ArchiSteamFarm {
|
||||
|
||||
internal bool CanReceiveSteamCards => !IsAccountLimited && !IsAccountLocked;
|
||||
internal bool HasMobileAuthenticator => BotDatabase?.MobileAuthenticator != null;
|
||||
internal bool IsConnectedAndLoggedOn => (SteamClient?.IsConnected == true) && (SteamClient.SteamID != null);
|
||||
internal bool IsConnectedAndLoggedOn => SteamID != 0;
|
||||
internal bool IsPlayingPossible => !PlayingBlocked && (LibraryLockedBySteamID == 0);
|
||||
|
||||
[JsonProperty]
|
||||
@@ -459,12 +459,30 @@ namespace ArchiSteamFarm {
|
||||
}
|
||||
}
|
||||
|
||||
internal bool IsMaster(ulong steamID) {
|
||||
if (steamID == 0) {
|
||||
ArchiLogger.LogNullError(nameof(steamID));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (IsOwner(steamID)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return GetSteamUserPermission(steamID) >= BotConfig.EPermission.Master;
|
||||
}
|
||||
|
||||
internal async Task LootIfNeeded() {
|
||||
if (!BotConfig.SendOnFarmingFinished || (BotConfig.SteamMasterID == 0) || !IsConnectedAndLoggedOn || (BotConfig.SteamMasterID == SteamClient.SteamID)) {
|
||||
if (!IsConnectedAndLoggedOn || !BotConfig.SendOnFarmingFinished) {
|
||||
return;
|
||||
}
|
||||
|
||||
await ResponseLoot(BotConfig.SteamMasterID).ConfigureAwait(false);
|
||||
ulong steamMasterID = GetFirstSteamMasterID();
|
||||
if (steamMasterID == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await ResponseLoot(steamMasterID).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
internal async Task OnFarmingFinished(bool farmedSomething) {
|
||||
@@ -549,7 +567,7 @@ namespace ArchiSteamFarm {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (await ArchiWebHandler.Init(SteamClient.SteamID, SteamClient.ConnectedUniverse, callback.Nonce, BotConfig.SteamParentalPIN).ConfigureAwait(false)) {
|
||||
if (await ArchiWebHandler.Init(SteamID, SteamClient.ConnectedUniverse, callback.Nonce, BotConfig.SteamParentalPIN).ConfigureAwait(false)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -866,6 +884,18 @@ namespace ArchiSteamFarm {
|
||||
return result;
|
||||
}
|
||||
|
||||
private ulong GetFirstSteamMasterID() => BotConfig.SteamUserPermissions.Where(kv => (kv.Key != SteamID) && (kv.Value == BotConfig.EPermission.Master)).Select(kv => kv.Key).OrderBy(steamID => steamID).FirstOrDefault();
|
||||
|
||||
private BotConfig.EPermission GetSteamUserPermission(ulong steamID) {
|
||||
if (steamID == 0) {
|
||||
ArchiLogger.LogNullError(nameof(steamID));
|
||||
return BotConfig.EPermission.None;
|
||||
}
|
||||
|
||||
BotConfig.EPermission permission;
|
||||
return BotConfig.SteamUserPermissions.TryGetValue(steamID, out permission) ? permission : BotConfig.EPermission.None;
|
||||
}
|
||||
|
||||
private void HandleCallbacks() {
|
||||
TimeSpan timeSpan = TimeSpan.FromMilliseconds(CallbackSleep);
|
||||
while (KeepRunning || SteamClient.IsConnected) {
|
||||
@@ -1021,24 +1051,29 @@ namespace ArchiSteamFarm {
|
||||
private void InitModules() {
|
||||
CardsFarmer.SetInitialState(BotConfig.Paused);
|
||||
|
||||
if ((BotConfig.SendTradePeriod > 0) && (BotConfig.SteamMasterID != 0)) {
|
||||
TimeSpan delay = TimeSpan.FromHours(BotConfig.SendTradePeriod) + TimeSpan.FromMinutes(Bots.Count);
|
||||
TimeSpan period = TimeSpan.FromHours(BotConfig.SendTradePeriod);
|
||||
|
||||
if (SendItemsTimer == null) {
|
||||
SendItemsTimer = new Timer(
|
||||
async e => await ResponseLoot(BotConfig.SteamMasterID).ConfigureAwait(false),
|
||||
null,
|
||||
delay, // Delay
|
||||
period // Period
|
||||
);
|
||||
} else {
|
||||
SendItemsTimer.Change(delay, period);
|
||||
}
|
||||
} else if (SendItemsTimer != null) {
|
||||
if (SendItemsTimer != null) {
|
||||
SendItemsTimer.Dispose();
|
||||
SendItemsTimer = null;
|
||||
}
|
||||
|
||||
if (BotConfig.SendTradePeriod == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
ulong steamMasterID = BotConfig.SteamUserPermissions.Where(kv => kv.Value == BotConfig.EPermission.Master).Select(kv => kv.Key).FirstOrDefault();
|
||||
if (steamMasterID == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
TimeSpan delay = TimeSpan.FromHours(BotConfig.SendTradePeriod) + TimeSpan.FromMinutes(Bots.Count);
|
||||
TimeSpan period = TimeSpan.FromHours(BotConfig.SendTradePeriod);
|
||||
|
||||
SendItemsTimer = new Timer(
|
||||
async e => await ResponseLoot(steamMasterID).ConfigureAwait(false),
|
||||
null,
|
||||
delay, // Delay
|
||||
period // Period
|
||||
);
|
||||
}
|
||||
|
||||
private void InitPermanentConnectionFailure() {
|
||||
@@ -1061,22 +1096,17 @@ namespace ArchiSteamFarm {
|
||||
await Start().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private bool IsMaster(ulong steamID) {
|
||||
if (steamID != 0) {
|
||||
return (steamID == BotConfig.SteamMasterID) || IsOwner(steamID);
|
||||
private bool IsFamilySharing(ulong steamID) {
|
||||
if (steamID == 0) {
|
||||
ArchiLogger.LogNullError(nameof(steamID));
|
||||
return false;
|
||||
}
|
||||
|
||||
ArchiLogger.LogNullError(nameof(steamID));
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool IsOperator(ulong steamID) {
|
||||
if (steamID != 0) {
|
||||
return (steamID == BotConfig.SteamOperatorID) || IsMaster(steamID);
|
||||
if (IsOwner(steamID)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
ArchiLogger.LogNullError(nameof(steamID));
|
||||
return false;
|
||||
return SteamFamilySharingIDs.Contains(steamID) || (GetSteamUserPermission(steamID) >= BotConfig.EPermission.FamilySharing);
|
||||
}
|
||||
|
||||
private bool IsMasterClanID(ulong steamID) {
|
||||
@@ -1088,6 +1118,19 @@ namespace ArchiSteamFarm {
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool IsOperator(ulong steamID) {
|
||||
if (steamID == 0) {
|
||||
ArchiLogger.LogNullError(nameof(steamID));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (IsOwner(steamID)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return GetSteamUserPermission(steamID) >= BotConfig.EPermission.Operator;
|
||||
}
|
||||
|
||||
private static bool IsOwner(ulong steamID) {
|
||||
if (steamID != 0) {
|
||||
return (steamID == Program.GlobalConfig.SteamOwnerID) || (Debugging.IsDebugBuild && (steamID == SharedInfo.ArchiSteamID));
|
||||
@@ -1706,7 +1749,7 @@ namespace ArchiSteamFarm {
|
||||
return;
|
||||
}
|
||||
|
||||
if (callback.FriendID == SteamClient.SteamID) {
|
||||
if (callback.FriendID == SteamID) {
|
||||
Events.OnPersonaState(this, callback);
|
||||
Statistics?.OnPersonaState(callback).Forget();
|
||||
} else if ((callback.FriendID == LibraryLockedBySteamID) && (callback.GameID == 0)) {
|
||||
@@ -1749,13 +1792,13 @@ namespace ArchiSteamFarm {
|
||||
|
||||
// Ignore no status updates
|
||||
if (LibraryLockedBySteamID == 0) {
|
||||
if ((callback.LibraryLockedBySteamID == 0) || (callback.LibraryLockedBySteamID == SteamClient.SteamID)) {
|
||||
if ((callback.LibraryLockedBySteamID == 0) || (callback.LibraryLockedBySteamID == SteamID)) {
|
||||
return;
|
||||
}
|
||||
|
||||
LibraryLockedBySteamID = callback.LibraryLockedBySteamID;
|
||||
} else {
|
||||
if ((callback.LibraryLockedBySteamID != 0) && (callback.LibraryLockedBySteamID != SteamClient.SteamID)) {
|
||||
if ((callback.LibraryLockedBySteamID != 0) && (callback.LibraryLockedBySteamID != SteamID)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2096,7 +2139,7 @@ namespace ArchiSteamFarm {
|
||||
|
||||
private string ResponseHelp(ulong steamID) {
|
||||
if (steamID != 0) {
|
||||
return IsOperator(steamID) ? FormatBotResponse("https://github.com/" + SharedInfo.GithubRepo + "/wiki/Commands") : null;
|
||||
return IsFamilySharing(steamID) ? FormatBotResponse("https://github.com/" + SharedInfo.GithubRepo + "/wiki/Commands") : null;
|
||||
}
|
||||
|
||||
ArchiLogger.LogNullError(nameof(steamID));
|
||||
@@ -2175,11 +2218,12 @@ namespace ArchiSteamFarm {
|
||||
return FormatBotResponse(Strings.BotLootingTemporarilyDisabled);
|
||||
}
|
||||
|
||||
if (BotConfig.SteamMasterID == 0) {
|
||||
ulong targetSteamMasterID = GetFirstSteamMasterID();
|
||||
if (targetSteamMasterID == 0) {
|
||||
return FormatBotResponse(Strings.BotLootingMasterNotDefined);
|
||||
}
|
||||
|
||||
if (BotConfig.SteamMasterID == SteamClient.SteamID) {
|
||||
if (targetSteamMasterID == SteamID) {
|
||||
return FormatBotResponse(Strings.BotLootingYourself);
|
||||
}
|
||||
|
||||
@@ -2198,12 +2242,12 @@ namespace ArchiSteamFarm {
|
||||
return FormatBotResponse(Strings.BotLootingFailed);
|
||||
}
|
||||
|
||||
if (!await ArchiWebHandler.SendTradeOffer(inventory, BotConfig.SteamMasterID, BotConfig.SteamTradeToken).ConfigureAwait(false)) {
|
||||
if (!await ArchiWebHandler.SendTradeOffer(inventory, targetSteamMasterID, BotConfig.SteamTradeToken).ConfigureAwait(false)) {
|
||||
return FormatBotResponse(Strings.BotLootingFailed);
|
||||
}
|
||||
|
||||
await Task.Delay(3000).ConfigureAwait(false); // Sometimes we can be too fast for Steam servers to generate confirmations, wait a short moment
|
||||
await AcceptConfirmations(true, Steam.ConfirmationDetails.EType.Trade, BotConfig.SteamMasterID).ConfigureAwait(false);
|
||||
await AcceptConfirmations(true, Steam.ConfirmationDetails.EType.Trade, targetSteamMasterID).ConfigureAwait(false);
|
||||
return FormatBotResponse(Strings.BotLootingSuccess);
|
||||
}
|
||||
|
||||
@@ -2299,7 +2343,7 @@ namespace ArchiSteamFarm {
|
||||
|
||||
Dictionary<uint, string> ownedGames;
|
||||
if (await ArchiWebHandler.HasValidApiKey().ConfigureAwait(false)) {
|
||||
ownedGames = await ArchiWebHandler.GetOwnedGames(SteamClient.SteamID).ConfigureAwait(false);
|
||||
ownedGames = await ArchiWebHandler.GetOwnedGames(SteamID).ConfigureAwait(false);
|
||||
} else {
|
||||
ownedGames = await ArchiWebHandler.GetMyOwnedGames().ConfigureAwait(false);
|
||||
}
|
||||
@@ -2423,10 +2467,13 @@ namespace ArchiSteamFarm {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!IsMaster(steamID)) {
|
||||
if (sticky || !SteamFamilySharingIDs.Contains(steamID)) {
|
||||
return null;
|
||||
}
|
||||
BotConfig.EPermission permission = GetSteamUserPermission(steamID);
|
||||
if (permission < BotConfig.EPermission.FamilySharing) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (sticky && (permission < BotConfig.EPermission.Master)) {
|
||||
return FormatBotResponse(Strings.ErrorAccessDenied);
|
||||
}
|
||||
|
||||
if (!IsConnectedAndLoggedOn) {
|
||||
@@ -2439,7 +2486,7 @@ namespace ArchiSteamFarm {
|
||||
|
||||
await CardsFarmer.Pause(sticky).ConfigureAwait(false);
|
||||
|
||||
if (!SteamFamilySharingIDs.Contains(steamID)) {
|
||||
if (permission >= BotConfig.EPermission.Master) {
|
||||
return FormatBotResponse(Strings.BotAutomaticIdlingNowPaused);
|
||||
}
|
||||
|
||||
@@ -2913,7 +2960,7 @@ namespace ArchiSteamFarm {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!IsOperator(steamID)) {
|
||||
if (!IsFamilySharing(steamID)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -97,15 +97,13 @@ namespace ArchiSteamFarm {
|
||||
[JsonProperty(Required = Required.DisallowNull)]
|
||||
internal readonly ulong SteamMasterClanID = 0;
|
||||
|
||||
[JsonProperty(Required = Required.DisallowNull)]
|
||||
internal readonly ulong SteamMasterID = 0;
|
||||
|
||||
[JsonProperty(Required = Required.DisallowNull)]
|
||||
internal readonly ulong SteamOperatorID = 0;
|
||||
|
||||
[JsonProperty]
|
||||
internal readonly string SteamTradeToken = null;
|
||||
|
||||
[SuppressMessage("ReSharper", "CollectionNeverUpdated.Global")]
|
||||
[JsonProperty(Required = Required.DisallowNull)]
|
||||
internal readonly Dictionary<ulong, EPermission> SteamUserPermissions = new Dictionary<ulong, EPermission>();
|
||||
|
||||
[JsonProperty(Required = Required.DisallowNull)]
|
||||
internal readonly ETradingPreferences TradingPreferences = ETradingPreferences.None;
|
||||
|
||||
@@ -178,6 +176,13 @@ namespace ArchiSteamFarm {
|
||||
NamesDescending
|
||||
}
|
||||
|
||||
internal enum EPermission : byte {
|
||||
None,
|
||||
FamilySharing,
|
||||
Operator,
|
||||
Master
|
||||
}
|
||||
|
||||
[Flags]
|
||||
internal enum ERedeemingPreferences : byte {
|
||||
None = 0,
|
||||
|
||||
11
ArchiSteamFarm/Localization/Strings.Designer.cs
generated
11
ArchiSteamFarm/Localization/Strings.Designer.cs
generated
@@ -376,7 +376,7 @@ namespace ArchiSteamFarm.Localization {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wyszukuje zlokalizowany ciąg podobny do ciągu Trade couldn't be sent because SteamMasterID is not defined!.
|
||||
/// Wyszukuje zlokalizowany ciąg podobny do ciągu Trade couldn't be sent because there is no master user defined!.
|
||||
/// </summary>
|
||||
internal static string BotLootingMasterNotDefined {
|
||||
get {
|
||||
@@ -718,6 +718,15 @@ namespace ArchiSteamFarm.Localization {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wyszukuje zlokalizowany ciąg podobny do ciągu Access denied!.
|
||||
/// </summary>
|
||||
internal static string ErrorAccessDenied {
|
||||
get {
|
||||
return ResourceManager.GetString("ErrorAccessDenied", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wyszukuje zlokalizowany ciąg podobny do ciągu Your bot config is invalid. Please verify content of {0} and try again!.
|
||||
/// </summary>
|
||||
|
||||
@@ -549,7 +549,7 @@ StackTrace:
|
||||
<value>Trade offer failed!</value>
|
||||
</data>
|
||||
<data name="BotLootingMasterNotDefined" xml:space="preserve">
|
||||
<value>Trade couldn't be sent because SteamMasterID is not defined!</value>
|
||||
<value>Trade couldn't be sent because there is no master user defined!</value>
|
||||
<comment>SteamMasterID is name of bot config property, it should not be translated</comment>
|
||||
</data>
|
||||
<data name="BotLootingNoLootableTypes" xml:space="preserve">
|
||||
@@ -714,4 +714,7 @@ StackTrace:
|
||||
<value>Owned already: {0}</value>
|
||||
<comment>{0} will be replaced by game's ID (number), {1} will be replaced by game's name</comment>
|
||||
</data>
|
||||
<data name="ErrorAccessDenied" xml:space="preserve">
|
||||
<value>Access denied!</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -178,7 +178,7 @@ namespace ArchiSteamFarm {
|
||||
}
|
||||
|
||||
// Always accept trades from SteamMasterID
|
||||
if ((tradeOffer.OtherSteamID64 != 0) && ((tradeOffer.OtherSteamID64 == Bot.BotConfig.SteamMasterID) || (tradeOffer.OtherSteamID64 == Program.GlobalConfig.SteamOwnerID))) {
|
||||
if ((tradeOffer.OtherSteamID64 != 0) && Bot.IsMaster(tradeOffer.OtherSteamID64)) {
|
||||
return new ParseTradeResult(tradeOffer.TradeOfferID, tradeOffer.ItemsToGive.Count > 0 ? ParseTradeResult.EResult.AcceptedWithItemLose : ParseTradeResult.EResult.AcceptedWithoutItemLose);
|
||||
}
|
||||
|
||||
|
||||
@@ -23,10 +23,9 @@
|
||||
"ShutdownOnFarmingFinished": false,
|
||||
"SteamLogin": null,
|
||||
"SteamMasterClanID": 0,
|
||||
"SteamMasterID": 0,
|
||||
"SteamOperatorID": 0,
|
||||
"SteamParentalPIN": "0",
|
||||
"SteamPassword": null,
|
||||
"SteamTradeToken": null,
|
||||
"SteamUserPermissions": {},
|
||||
"TradingPreferences": 0
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ using System.Drawing.Design;
|
||||
using System.IO;
|
||||
using ConfigGenerator.JSON;
|
||||
using Newtonsoft.Json;
|
||||
using Wexman.Design;
|
||||
|
||||
namespace ConfigGenerator {
|
||||
[SuppressMessage("ReSharper", "AutoPropertyCanBeMadeGetOnly.Global")]
|
||||
@@ -112,14 +113,6 @@ namespace ConfigGenerator {
|
||||
[JsonProperty(Required = Required.DisallowNull)]
|
||||
public ulong SteamMasterClanID { get; set; } = 0;
|
||||
|
||||
[LocalizedCategory("Access")]
|
||||
[JsonProperty(Required = Required.DisallowNull)]
|
||||
public ulong SteamMasterID { get; set; } = 0;
|
||||
|
||||
[LocalizedCategory("Access")]
|
||||
[JsonProperty(Required = Required.DisallowNull)]
|
||||
public ulong SteamOperatorID { get; set; } = 0;
|
||||
|
||||
[LocalizedCategory("Access")]
|
||||
[JsonProperty]
|
||||
public string SteamParentalPIN { get; set; } = "0";
|
||||
@@ -133,6 +126,11 @@ namespace ConfigGenerator {
|
||||
[JsonProperty]
|
||||
public string SteamTradeToken { get; set; } = null;
|
||||
|
||||
[LocalizedCategory("Access")]
|
||||
[Editor(typeof(GenericDictionaryEditor<ulong, EPermission>), typeof(UITypeEditor))]
|
||||
[JsonProperty(Required = Required.DisallowNull)]
|
||||
public Dictionary<ulong, EPermission> SteamUserPermissions { get; set; } = new Dictionary<ulong, EPermission>();
|
||||
|
||||
[LocalizedCategory("Advanced")]
|
||||
[Editor(typeof(FlagEnumUiEditor), typeof(UITypeEditor))]
|
||||
[JsonProperty(Required = Required.DisallowNull)]
|
||||
@@ -195,6 +193,13 @@ namespace ConfigGenerator {
|
||||
NamesDescending
|
||||
}
|
||||
|
||||
internal enum EPermission : byte {
|
||||
None,
|
||||
FamilySharing,
|
||||
Operator,
|
||||
Master
|
||||
}
|
||||
|
||||
[Flags]
|
||||
internal enum ERedeemingPreferences : byte {
|
||||
None = 0,
|
||||
|
||||
@@ -46,6 +46,9 @@
|
||||
<RunPostBuildEvent>OnOutputUpdated</RunPostBuildEvent>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="GenericDictionaryEditor, Version=1.1.0.0, Culture=neutral, PublicKeyToken=7f1cce5280f1f8eb, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\GenDictEdit.1.1.0\lib\net20\GenericDictionaryEditor.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Newtonsoft.Json, Version=10.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\Newtonsoft.Json.10.0.1-beta1\lib\net45\Newtonsoft.Json.dll</HintPath>
|
||||
</Reference>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<packages>
|
||||
<package id="Costura.Fody" version="2.0.0-beta0018" targetFramework="net461" developmentDependency="true" />
|
||||
<package id="Fody" version="1.30.0-beta01" targetFramework="net461" developmentDependency="true" />
|
||||
<package id="Newtonsoft.Json" version="10.0.1-beta1" targetFramework="net461" />
|
||||
<package id="Resource.Embedder" version="1.2.2" targetFramework="net461" developmentDependency="true" />
|
||||
<package id="Costura.Fody" version="2.0.0-beta0018" targetFramework="net461" developmentDependency="true" />
|
||||
<package id="Fody" version="1.30.0-beta01" targetFramework="net461" developmentDependency="true" />
|
||||
<package id="GenDictEdit" version="1.1.0" targetFramework="net461" />
|
||||
<package id="Newtonsoft.Json" version="10.0.1-beta1" targetFramework="net461" />
|
||||
<package id="Resource.Embedder" version="1.2.2" targetFramework="net461" developmentDependency="true" />
|
||||
</packages>
|
||||
BIN
packages/GenDictEdit.1.1.0/GenDictEdit.1.1.0.nupkg
vendored
Normal file
BIN
packages/GenDictEdit.1.1.0/GenDictEdit.1.1.0.nupkg
vendored
Normal file
Binary file not shown.
BIN
packages/GenDictEdit.1.1.0/lib/net20/GenericDictionaryEditor.dll
vendored
Normal file
BIN
packages/GenDictEdit.1.1.0/lib/net20/GenericDictionaryEditor.dll
vendored
Normal file
Binary file not shown.
Reference in New Issue
Block a user