added top player stats
fix for some commands returning multiple matches found when target not required
4
.gitignore
vendored
@ -223,4 +223,6 @@ global.min.js
|
||||
bootstrap-custom.css
|
||||
bootstrap-custom.min.css
|
||||
**/Master/static
|
||||
**/Master/dev_env
|
||||
**/Master/dev_env
|
||||
/WebfrontCore/Views/Plugins/Stats
|
||||
/WebfrontCore/wwwroot/images/icons
|
||||
|
@ -20,6 +20,7 @@ using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Text;
|
||||
using IW4MAdmin.Application.API.Master;
|
||||
using System.Reflection;
|
||||
|
||||
namespace IW4MAdmin.Application
|
||||
{
|
||||
@ -510,5 +511,7 @@ namespace IW4MAdmin.Application
|
||||
{
|
||||
OnEvent.Set();
|
||||
}
|
||||
|
||||
public IList<Assembly> GetPluginAssemblies() => SharedLibraryCore.Plugins.PluginImporter.PluginAssemblies;
|
||||
}
|
||||
}
|
||||
|
@ -308,7 +308,7 @@ namespace IW4MAdmin
|
||||
|
||||
List<Player> matchingPlayers;
|
||||
|
||||
if (E.Target == null) // Find active player including quotes (multiple words)
|
||||
if (E.Target == null && C.RequiresTarget) // Find active player including quotes (multiple words)
|
||||
{
|
||||
matchingPlayers = GetClientByName(E.Data.Trim());
|
||||
if (matchingPlayers.Count > 1)
|
||||
@ -333,7 +333,7 @@ namespace IW4MAdmin
|
||||
}
|
||||
}
|
||||
|
||||
if (E.Target == null) // Find active player as single word
|
||||
if (E.Target == null && C.RequiresTarget) // Find active player as single word
|
||||
{
|
||||
matchingPlayers = GetClientByName(Args[0]);
|
||||
if (matchingPlayers.Count > 1)
|
||||
|
@ -1,5 +1,8 @@
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Objects;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace IW4ScriptCommands.Commands
|
||||
@ -12,6 +15,48 @@ namespace IW4ScriptCommands.Commands
|
||||
|
||||
public override async Task ExecuteAsync(GameEvent E)
|
||||
{
|
||||
List<string> teamAssignments = new List<string>();
|
||||
|
||||
var clients = E.Owner.GetPlayersAsList().Select(c => new
|
||||
{
|
||||
Num = c.ClientNumber,
|
||||
Elo = IW4MAdmin.Plugins.Stats.Plugin.Manager.GetClientStats(c.ClientId, E.Owner.GetHashCode()).EloRating,
|
||||
CurrentTeam = IW4MAdmin.Plugins.Stats.Plugin.Manager.GetClientStats(c.ClientId, E.Owner.GetHashCode()).Team
|
||||
})
|
||||
.OrderByDescending(c => c.Elo)
|
||||
.ToList();
|
||||
|
||||
int team = 0;
|
||||
for (int i = 0; i < clients.Count(); i++)
|
||||
{
|
||||
if (i == 0)
|
||||
{
|
||||
team = 1;
|
||||
continue;
|
||||
}
|
||||
if (i == 1)
|
||||
{
|
||||
team = 2;
|
||||
continue;
|
||||
}
|
||||
if (i == 2)
|
||||
{
|
||||
team = 2;
|
||||
continue;
|
||||
}
|
||||
if (i % 2 == 0)
|
||||
{
|
||||
if (team == 1)
|
||||
team = 2;
|
||||
else
|
||||
team = 1;
|
||||
}
|
||||
|
||||
teamAssignments.Add($"{clients[i].Num},{team}");
|
||||
}
|
||||
|
||||
string args = string.Join(",", teamAssignments);
|
||||
await E.Owner.SetDvarAsync("sv_iw4madmin_commandargs", args);
|
||||
await E.Owner.ExecuteCommandAsync("sv_iw4madmin_command balance");
|
||||
await E.Origin.Tell("Balance command sent");
|
||||
}
|
||||
|
@ -13,6 +13,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj" />
|
||||
<ProjectReference Include="..\Stats\Stats.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -27,7 +27,7 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
|
||||
public const int HighSampleMinKills = 100;
|
||||
public const double KillTimeThreshold = 0.2;
|
||||
|
||||
public const double MaxStrainBan = 0.4;
|
||||
public const double MaxStrainBan = 1.12;
|
||||
public const double MaxOffset = 1.2;
|
||||
public const double MaxStrainFlag = 0.36;
|
||||
|
||||
|
@ -4,7 +4,7 @@ using System.Collections.Generic;
|
||||
|
||||
namespace IW4MAdmin.Plugins.Stats.Config
|
||||
{
|
||||
class StatsConfiguration : IBaseConfiguration
|
||||
public class StatsConfiguration : IBaseConfiguration
|
||||
{
|
||||
public bool EnableAntiCheat { get; set; }
|
||||
public List<StreakMessageConfiguration> KillstreakMessages { get; set; }
|
||||
|
@ -11,6 +11,9 @@ using SharedLibraryCore.Objects;
|
||||
using SharedLibraryCore.Commands;
|
||||
using IW4MAdmin.Plugins.Stats.Models;
|
||||
using System.Text.RegularExpressions;
|
||||
using IW4MAdmin.Plugins.Stats.Web.Dtos;
|
||||
using SharedLibraryCore.Database;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
{
|
||||
@ -36,6 +39,90 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
Servers = null;
|
||||
}
|
||||
|
||||
public EFClientStatistics GetClientStats(int clientId, int serverId) => Servers[serverId].PlayerStats[clientId];
|
||||
|
||||
public async Task<List<TopStatsInfo>> GetTopStats(int start, int count)
|
||||
{
|
||||
using (var context = new DatabaseContext())
|
||||
{
|
||||
context.ChangeTracker.AutoDetectChangesEnabled = false;
|
||||
context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
|
||||
|
||||
var thirtyDaysAgo = DateTime.UtcNow.AddMonths(-1);
|
||||
var iqClientIds = (from stat in context.Set<EFClientStatistics>()
|
||||
join client in context.Clients
|
||||
on stat.ClientId equals client.ClientId
|
||||
#if !DEBUG
|
||||
where stat.TimePlayed >= 3600
|
||||
where client.Level != Player.Permission.Banned
|
||||
where client.LastConnection >= thirtyDaysAgo
|
||||
where stat.Performance > 60
|
||||
#endif
|
||||
group stat by stat.ClientId into s
|
||||
orderby s.Average(cs => cs.Performance) descending
|
||||
select s.First().ClientId)
|
||||
.Skip(start)
|
||||
.Take(count);
|
||||
|
||||
var clientIds = await iqClientIds.ToListAsync();
|
||||
|
||||
var iqStats = (from stat in context.Set<EFClientStatistics>()
|
||||
join client in context.Clients
|
||||
on stat.ClientId equals client.ClientId
|
||||
where clientIds.Contains(client.ClientId)
|
||||
select new
|
||||
{
|
||||
client.CurrentAlias.Name,
|
||||
client.ClientId,
|
||||
stat.Kills,
|
||||
stat.Deaths,
|
||||
stat.EloRating,
|
||||
stat.Skill,
|
||||
stat.TimePlayed,
|
||||
client.LastConnection,
|
||||
client.TotalConnectionTime
|
||||
});
|
||||
|
||||
var stats = await iqStats.ToListAsync();
|
||||
|
||||
var groupedSelection = stats.GroupBy(s => s.ClientId).Select(s =>
|
||||
new TopStatsInfo()
|
||||
{
|
||||
Name = s.Select(c => c.Name).FirstOrDefault(),
|
||||
// weighted based on time played
|
||||
Performance = Math.Round
|
||||
(s
|
||||
.Where(c => (c.Skill + c.EloRating) / 2.0 > 0)
|
||||
.Sum(c => (c.Skill + c.EloRating) / 2.0 * c.TimePlayed) /
|
||||
s.Where(c => (c.Skill + c.EloRating) / 2.0 > 0)
|
||||
.Sum(c => c.TimePlayed), 2),
|
||||
// ditto
|
||||
KDR = Math.Round(s
|
||||
.Where(c => c.Deaths > 0)
|
||||
.Sum(c => ((c.Kills / (double)c.Deaths) * c.TimePlayed) /
|
||||
s.Where(d => d.Deaths > 0)
|
||||
.Sum(d => d.TimePlayed)), 2),
|
||||
ClientId = s.Select(c => c.ClientId).FirstOrDefault(),
|
||||
Deaths = s.Sum(cs => cs.Deaths),
|
||||
Kills = s.Sum(cs => cs.Kills),
|
||||
LastSeen = Utilities.GetTimePassed(s.First().LastConnection, false),
|
||||
TimePlayed = Math.Round(s.First().TotalConnectionTime / 3600.0, 1).ToString("#,##0"),
|
||||
});
|
||||
|
||||
var statList = groupedSelection.OrderByDescending(s => s.Performance).ToList();
|
||||
|
||||
// set the ranking numerically
|
||||
int i = start + 1;
|
||||
foreach (var stat in statList)
|
||||
{
|
||||
stat.Ranking = i;
|
||||
i++;
|
||||
}
|
||||
|
||||
return statList;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a server to the StatManager server pool
|
||||
/// </summary>
|
||||
@ -495,35 +582,35 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
// calulate elo
|
||||
if (Servers[attackerStats.ServerId].PlayerStats.Count > 1)
|
||||
{
|
||||
/* var validAttackerLobbyRatings = Servers[attackerStats.ServerId].PlayerStats
|
||||
.Where(cs => cs.Value.ClientId != attackerStats.ClientId)
|
||||
.Where(cs =>
|
||||
Servers[attackerStats.ServerId].IsTeamBased ?
|
||||
cs.Value.Team != attackerStats.Team :
|
||||
cs.Value.Team != IW4Info.Team.Spectator)
|
||||
.Where(cs => cs.Value.Team != IW4Info.Team.Spectator);
|
||||
|
||||
double attackerLobbyRating = validAttackerLobbyRatings.Count() > 0 ?
|
||||
validAttackerLobbyRatings.Average(cs => cs.Value.EloRating) :
|
||||
attackerStats.EloRating;
|
||||
|
||||
var validVictimLobbyRatings = Servers[victimStats.ServerId].PlayerStats
|
||||
.Where(cs => cs.Value.ClientId != victimStats.ClientId)
|
||||
.Where(cs =>
|
||||
Servers[attackerStats.ServerId].IsTeamBased ?
|
||||
cs.Value.Team != victimStats.Team :
|
||||
cs.Value.Team != IW4Info.Team.Spectator)
|
||||
/* var validAttackerLobbyRatings = Servers[attackerStats.ServerId].PlayerStats
|
||||
.Where(cs => cs.Value.ClientId != attackerStats.ClientId)
|
||||
.Where(cs =>
|
||||
Servers[attackerStats.ServerId].IsTeamBased ?
|
||||
cs.Value.Team != attackerStats.Team :
|
||||
cs.Value.Team != IW4Info.Team.Spectator)
|
||||
.Where(cs => cs.Value.Team != IW4Info.Team.Spectator);
|
||||
|
||||
double victimLobbyRating = validVictimLobbyRatings.Count() > 0 ?
|
||||
validVictimLobbyRatings.Average(cs => cs.Value.EloRating) :
|
||||
victimStats.EloRating;*/
|
||||
double attackerLobbyRating = validAttackerLobbyRatings.Count() > 0 ?
|
||||
validAttackerLobbyRatings.Average(cs => cs.Value.EloRating) :
|
||||
attackerStats.EloRating;
|
||||
|
||||
var validVictimLobbyRatings = Servers[victimStats.ServerId].PlayerStats
|
||||
.Where(cs => cs.Value.ClientId != victimStats.ClientId)
|
||||
.Where(cs =>
|
||||
Servers[attackerStats.ServerId].IsTeamBased ?
|
||||
cs.Value.Team != victimStats.Team :
|
||||
cs.Value.Team != IW4Info.Team.Spectator)
|
||||
.Where(cs => cs.Value.Team != IW4Info.Team.Spectator);
|
||||
|
||||
double victimLobbyRating = validVictimLobbyRatings.Count() > 0 ?
|
||||
validVictimLobbyRatings.Average(cs => cs.Value.EloRating) :
|
||||
victimStats.EloRating;*/
|
||||
|
||||
double attackerEloDifference = Math.Log(Math.Max(1, victimStats.EloRating)) - Math.Log(Math.Max(1, attackerStats.EloRating));
|
||||
double winPercentage = 1.0 / (1 + Math.Pow(10, attackerEloDifference / Math.E));
|
||||
|
||||
// double victimEloDifference = Math.Log(Math.Max(1, attackerStats.EloRating)) - Math.Log(Math.Max(1, victimStats.EloRating));
|
||||
// double lossPercentage = 1.0 / (1 + Math.Pow(10, victimEloDifference/ Math.E));
|
||||
// double victimEloDifference = Math.Log(Math.Max(1, attackerStats.EloRating)) - Math.Log(Math.Max(1, victimStats.EloRating));
|
||||
// double lossPercentage = 1.0 / (1 + Math.Pow(10, victimEloDifference/ Math.E));
|
||||
|
||||
attackerStats.EloRating += 6.0 * (1 - winPercentage);
|
||||
victimStats.EloRating -= 6.0 * (1 - winPercentage);
|
||||
|
@ -16,7 +16,7 @@ using IW4MAdmin.Plugins.Stats.Models;
|
||||
|
||||
namespace IW4MAdmin.Plugins.Stats
|
||||
{
|
||||
class Plugin : IPlugin
|
||||
public class Plugin : IPlugin
|
||||
{
|
||||
public string Name => "Simple Stats";
|
||||
|
||||
|
@ -14,12 +14,9 @@
|
||||
<Configurations>Debug;Release;Prerelease</Configurations>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="Cheat\Strain.cs~RF16f7b3.TMP" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj" />
|
||||
<ProjectReference Include="..\..\WebfrontCore\WebfrontCore.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@ -30,4 +27,8 @@
|
||||
<Exec Command="copy "$(TargetPath)" "$(SolutionDir)BUILD\Plugins"" />
|
||||
</Target>
|
||||
|
||||
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
|
||||
<Exec Command="xcopy /E /K /Y /C /I "$(ProjectDir)Web\Views" "$(SolutionDir)WebfrontCore\Views\Plugins"
xcopy /E /K /Y /C /I "$(ProjectDir)Web\wwwroot\images" "$(SolutionDir)WebfrontCore\wwwroot\images"" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
|
28
Plugins/Stats/Web/Controllers/StatsController.cs
Normal file
@ -0,0 +1,28 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SharedLibraryCore;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using WebfrontCore.Controllers;
|
||||
|
||||
namespace IW4MAdmin.Plugins.Stats.Web.Controllers
|
||||
{
|
||||
public class StatsController : BaseController
|
||||
{
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> TopPlayersAsync()
|
||||
{
|
||||
ViewBag.Title = Utilities.CurrentLocalization.LocalizationIndex.Set["WEBFRONT_STATS_INDEX_TITLE"];
|
||||
ViewBag.Description = Utilities.CurrentLocalization.LocalizationIndex.Set["WEBFRONT_STATS_INDEX_DESC"];
|
||||
|
||||
return View("Index", await Plugin.Manager.GetTopStats(0, 15));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetTopPlayersAsync(int count, int offset)
|
||||
{
|
||||
return View("_List", await Plugin.Manager.GetTopStats(offset, count));
|
||||
}
|
||||
}
|
||||
}
|
20
Plugins/Stats/Web/Dtos/TopStatsInfo.cs
Normal file
@ -0,0 +1,20 @@
|
||||
using SharedLibraryCore.Dtos;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace IW4MAdmin.Plugins.Stats.Web.Dtos
|
||||
{
|
||||
public class TopStatsInfo : SharedInfo
|
||||
{
|
||||
public int Ranking { get; set; }
|
||||
public string Name { get; set; }
|
||||
public int ClientId { get; set; }
|
||||
public double KDR { get; set; }
|
||||
public double Performance { get; set; }
|
||||
public string TimePlayed { get; set; }
|
||||
public string LastSeen { get; set; }
|
||||
public int Kills { get; set; }
|
||||
public int Deaths { get; set; }
|
||||
}
|
||||
}
|
13
Plugins/Stats/Web/Views/Stats/Index.cshtml
Normal file
@ -0,0 +1,13 @@
|
||||
@model List<IW4MAdmin.Plugins.Stats.Web.Dtos.TopStatsInfo>
|
||||
<h4 class="pb-2 text-center ">@ViewBag.Title</h4>
|
||||
|
||||
<div id="stats_top_players" class="row border-top border-bottom">
|
||||
@await Html.PartialAsync("_List", Model)
|
||||
</div>
|
||||
|
||||
@section scripts {
|
||||
<environment include="Development">
|
||||
<script type="text/javascript" src="~/js/loader.js"></script>
|
||||
</environment>
|
||||
<script>initLoader('/Stats/GetTopPlayersAsync', '#stats_top_players');</script>
|
||||
}
|
50
Plugins/Stats/Web/Views/Stats/_List.cshtml
Normal file
@ -0,0 +1,50 @@
|
||||
@model List<IW4MAdmin.Plugins.Stats.Web.Dtos.TopStatsInfo>
|
||||
@{
|
||||
Layout = null;
|
||||
var loc = SharedLibraryCore.Utilities.CurrentLocalization.LocalizationIndex.Set;
|
||||
double getDeviation(double deviations) => Math.Pow(Math.E, 5.0813 + (deviations * 0.8694));
|
||||
string rankIcon(double elo)
|
||||
{
|
||||
if (elo >= getDeviation(-1) && elo < getDeviation(-0.25))
|
||||
return "0_no-place/menu_div_no_place.png";
|
||||
if (elo >= getDeviation(-0.25) && elo < getDeviation(0.25))
|
||||
return "1_iron/menu_div_iron_sub03.png";
|
||||
if (elo >= getDeviation(0.25) && elo < getDeviation(0.6875))
|
||||
return "2_bronze/menu_div_bronze_sub03.png";
|
||||
if (elo >= getDeviation(0.6875) && elo < getDeviation(1))
|
||||
return "3_silver/menu_div_silver_sub03.png";
|
||||
if (elo >= getDeviation(1) && elo < getDeviation(1.25))
|
||||
return "4_gold/menu_div_gold_sub03.png";
|
||||
if (elo >= getDeviation(1.25) && elo < getDeviation(1.5))
|
||||
return "5_platinum/menu_div_platinum_sub03.png";
|
||||
if (elo >= getDeviation(1.5) && elo < getDeviation(1.75))
|
||||
return "6_semipro/menu_div_semipro_sub03.png";
|
||||
if (elo >= getDeviation(1.75))
|
||||
return "7_pro/menu_div_pro_sub03.png";
|
||||
|
||||
return "0_no-place/menu_div_no_place.png";
|
||||
}
|
||||
}
|
||||
<table class="table table-striped mb-0" style="background-color:rgba(0, 0, 0, 0.1)">
|
||||
@foreach (var stat in Model)
|
||||
{
|
||||
<tr>
|
||||
<td style="vertical-align: middle">
|
||||
<div class="">
|
||||
<h2 class="text-muted">#@stat.Ranking — @Html.ActionLink(stat.Name, "ProfileAsync", "Client", new { id = stat.ClientId })</h2>
|
||||
<span class="text-primary">@stat.Performance</span><span class="text-muted"> @loc["PLUGINS_STATS_COMMANDS_PERFORMANCE"]</span><br />
|
||||
<span class="text-primary">@stat.KDR</span><span class="text-muted"> @loc["PLUGINS_STATS_TEXT_KDR"]</span>
|
||||
<span class="text-primary">@stat.Kills</span><span class="text-muted"> @loc["PLUGINS_STATS_TEXT_KILLS"]</span>
|
||||
<span class="text-primary">@stat.Deaths</span><span class="text-muted"> @loc["PLUGINS_STATS_TEXT_DEATHS"]</span><br />
|
||||
<span class="text-muted">@loc["WEBFRONT_PROFILE_PLAYER"]</span> <span class="text-primary"> @stat.TimePlayed </span><span class="text-muted">@loc["GLOBAL_HOURS"]</span><br />
|
||||
<span class="text-muted">@loc["WEBFRONT_PROFILE_LSEEN"]</span><span class="text-primary"> @stat.LastSeen </span><span class="text-muted">@loc["WEBFRONT_PENALTY_TEMPLATE_AGO"]</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-right ml-0 pl-0" style="vertical-align: middle">
|
||||
<div>
|
||||
<img src="/images/icons/@rankIcon(stat.Performance)" />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 1.5 KiB |
BIN
Plugins/Stats/Web/wwwroot/images/icons/1_iron/menu_div_iron.png
Normal file
After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 1.9 KiB |
BIN
Plugins/Stats/Web/wwwroot/images/icons/4_gold/menu_div_gold.png
Normal file
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 2.1 KiB |