├── .editorconfig
├── .gitattributes
├── .gitignore
├── .gitmodules
├── DstServerQuery.EntityFrameworkCore
├── DatabaseType.cs
├── DstServerQuery.EntityFrameworkCore.csproj
├── Helpers
│ └── DateTimeOffsetUtcConverter.cs
├── HistoryCountService.cs
└── Model
│ ├── DstDbContext.cs
│ ├── Entities
│ ├── ColorItem.cs
│ ├── DstPlayer.cs
│ ├── DstServerHistory.cs
│ └── ServerCountInfo.cs
│ └── SimpleCacheDatabase.cs
├── DstServerQuery.Web
├── .config
│ └── dotnet-tools.json
├── ConfigureSwaggerOptions.cs
├── Controllers
│ ├── V1
│ │ └── ApiController.cs
│ └── V2
│ │ ├── HelperController.cs
│ │ ├── HistoryController.cs
│ │ ├── ModsController.cs
│ │ └── ServerController.cs
├── DstServerQuery.Web.csproj
├── DstServerQuery.Web.http
├── GeoLite2-City.mmdb
├── Helpers
│ ├── Commands
│ │ ├── AppcalitionCommand.cs
│ │ ├── CommandPromptCallbacks.cs
│ │ ├── DstCommand.cs
│ │ └── GCCommand.cs
│ ├── Console
│ │ └── ControllableConsoleSink.cs
│ ├── DstPlayerEqualityComparer.cs
│ ├── Helper.cs
│ └── ServerQueryer
│ │ ├── JsonConverter
│ │ ├── RegexValueJsonConverter.cs
│ │ ├── StringArrayJsonConverter.cs
│ │ ├── StringSplitConverter.cs
│ │ └── ToStringJsonConverter.cs
│ │ ├── LobbyServerQueryerV1.cs
│ │ └── LobbyServerQueryerV2.cs
├── Migrations
│ ├── MySql
│ │ ├── 20231215085553_InitTo_AddDstPlayerIndex.Designer.cs
│ │ ├── 20231215085553_InitTo_AddDstPlayerIndex.cs
│ │ └── MySqlDstDbContextModelSnapshot.cs
│ ├── PostgreSql
│ │ ├── 20231215090257_InitTo_AddDstPlayerIndex.Designer.cs
│ │ ├── 20231215090257_InitTo_AddDstPlayerIndex.cs
│ │ └── PostgreSqlDstDbContextModelSnapshot.cs
│ ├── SqlServer
│ │ ├── 20231109061453_Init.Designer.cs
│ │ ├── 20231109061453_Init.cs
│ │ ├── 20231110030739_PlayerNotNull.Designer.cs
│ │ ├── 20231110030739_PlayerNotNull.cs
│ │ ├── 20231207104123_AddTagColor.Designer.cs
│ │ ├── 20231207104123_AddTagColor.cs
│ │ ├── 20231208134329_AddDateIndex.Designer.cs
│ │ ├── 20231208134329_AddDateIndex.cs
│ │ ├── 20231209045633_ToDateTimeOffset.Designer.cs
│ │ ├── 20231209045633_ToDateTimeOffset.cs
│ │ ├── 20231212090059_AddDstPlayerIndex.Designer.cs
│ │ ├── 20231212090059_AddDstPlayerIndex.cs
│ │ └── DstDbContextModelSnapshot.cs
│ └── Sqlite
│ │ ├── 20231215085623_InitTo_AddDstPlayerIndex.Designer.cs
│ │ ├── 20231215085623_InitTo_AddDstPlayerIndex.cs
│ │ └── SqliteDstDbContextModelSnapshot.cs
├── Models
│ ├── Configurations
│ │ ├── DstModsFileServiceOptions.cs
│ │ ├── DstVersionServiceOptions.cs
│ │ └── SteamOptions.cs
│ ├── DstModCount.cs
│ ├── DstModNameCount.cs
│ ├── DstServerQueryWeb.cs
│ ├── Http
│ │ ├── ColorResponse.cs
│ │ ├── GetPlayersResponse.cs
│ │ ├── GetServerVersionResponse.cs
│ │ ├── GetTotalResponse.cs
│ │ ├── ListResponse.cs
│ │ ├── Mods
│ │ │ ├── DstModsInfoResponse.cs
│ │ │ ├── DstModsNameUsageResponse.cs
│ │ │ ├── DstModsUsageResponse.cs
│ │ │ └── ModsQueryResponse.cs
│ │ ├── PlayerServerHistoryResponse.cs
│ │ ├── PrefabsResponse.cs
│ │ ├── ResponseBase.cs
│ │ ├── ServerCountHistoryResponse.cs
│ │ ├── ServerDetailsResponse.cs
│ │ ├── ServerHistoryResponse.cs
│ │ └── TagsResponse.cs
│ ├── PlayerInfoItem.cs
│ └── ServerHistoryItem.cs
├── Program.cs
├── Properties
│ └── launchSettings.json
├── Services
│ ├── AppHostedService.cs
│ ├── CommandService.cs
│ ├── DstHistoryService.cs
│ ├── DstModsFileHosedService.cs
│ ├── HistoryCleanupService.cs
│ └── TrafficRateLimiter
│ │ ├── TrafficChunk.cs
│ │ ├── TrafficContext.cs
│ │ ├── TrafficLimiterExtensions.cs
│ │ ├── TrafficMonitorStream.cs
│ │ ├── TrafficRateLimit.cs
│ │ └── TrafficRateLimitOptions.cs
├── appsettings.Development.json
├── appsettings.json
├── libman.json
├── migrate-dst.ps1
└── wwwroot
│ └── doc
│ └── swagger_ext.js
├── DstServerQuery.slnx
├── DstServerQuery
├── Converters
│ ├── GameModeConverter.cs
│ ├── IPAddressInfoConverter.cs
│ ├── IdCacheConverter.cs
│ ├── IntentConverter.cs
│ ├── LobbyDaysInfoConverter.cs
│ ├── LobbyGuidConverter.cs
│ ├── LobbyModsInfoConverter.cs
│ ├── LobbyNumberIdConverter.cs
│ ├── LobbyPlayersInfoConverter.cs
│ ├── LobbySeasonConverter.cs
│ ├── LobbySessionIdConverter.cs
│ ├── LobbySteamIdConverter.cs
│ ├── LobbyTagsConverter.cs
│ ├── LobbyWorldGenConverter.cs
│ └── LobbyWorldLevelConverter.cs
├── DstServerQuery.csproj
├── Helpers
│ ├── ConcurrentStringCacheDictionary.cs
│ ├── DstConverterHelper.cs
│ ├── DstEnumText.cs
│ ├── JsonSourceGeneraterContext.cs
│ ├── LuaTempEnvironment.cs
│ ├── StreamWriterHttpContent.cs
│ ├── TempUtf8JsonString.cs
│ └── Utils.cs
├── LobbyDownloader.cs
├── LobbyServerManager.cs
├── Models
│ ├── IPAddressInfo.cs
│ ├── LevelDataOverrideEnum.cs
│ ├── Lobby
│ │ ├── Interfaces
│ │ │ ├── V1
│ │ │ │ ├── ILobbyServerDetailedV1.cs
│ │ │ │ └── ILobbyServerV1.cs
│ │ │ └── V2
│ │ │ │ ├── ILobbyServerDetailedV2.cs
│ │ │ │ └── ILobbyServerV2.cs
│ │ ├── LobbyGameMode.cs
│ │ ├── LobbyGuid.cs
│ │ ├── LobbyIntent.cs
│ │ ├── LobbyNumberId.cs
│ │ ├── LobbySeason.cs
│ │ ├── LobbyServer.cs
│ │ ├── LobbyServerDetailed.cs
│ │ ├── LobbySessionId.cs
│ │ └── LobbySteamId.cs
│ ├── LobbyDaysInfo.cs
│ ├── LobbyGet.cs
│ ├── LobbyModInfo.cs
│ ├── LobbyPlayerInfo.cs
│ ├── Platform.cs
│ ├── Requests
│ │ └── DstWebConfig.cs
│ └── WorldLevelItem.cs
└── Services
│ ├── DstVersionService.cs
│ └── GeoIPService.cs
├── LICENSE.txt
├── README.md
├── publish-linux.bat
└── publish-win.bat
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.{cs,vb}]
2 |
3 | # IDE0305: 简化集合初始化
4 | dotnet_diagnostic.IDE0305.severity = none
5 | end_of_line = crlf
6 | dotnet_style_qualification_for_field = false:silent
7 | dotnet_style_qualification_for_property = false:silent
8 | dotnet_style_qualification_for_method = false:silent
9 | dotnet_style_qualification_for_event = false:silent
10 | dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
11 | dotnet_style_operator_placement_when_wrapping = beginning_of_line
12 | tab_width = 4
13 | indent_size = 4
14 | dotnet_style_coalesce_expression = true:suggestion
15 | dotnet_style_null_propagation = true:suggestion
16 | dotnet_style_prefer_is_null_check_over_reference_equality_method = false:silent
17 | dotnet_style_prefer_auto_properties = true:silent
18 | dotnet_style_object_initializer = true:silent
19 | dotnet_style_collection_initializer = true:suggestion
20 | dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
21 | dotnet_style_prefer_conditional_expression_over_assignment = true:silent
22 | dotnet_style_prefer_conditional_expression_over_return = true:silent
23 | dotnet_style_explicit_tuple_names = true:suggestion
24 | dotnet_style_prefer_inferred_tuple_names = true:suggestion
25 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
26 | dotnet_style_prefer_simplified_interpolation = true:suggestion
27 | dotnet_style_prefer_compound_assignment = true:suggestion
28 | dotnet_style_namespace_match_folder = true:suggestion
29 | dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion
30 |
31 | [*.cs]
32 | csharp_using_directive_placement = outside_namespace:silent
33 | csharp_style_expression_bodied_methods = true:silent
34 | csharp_style_expression_bodied_constructors = false:silent
35 | csharp_style_expression_bodied_operators = false:silent
36 | csharp_style_expression_bodied_properties = true:silent
37 | csharp_style_expression_bodied_indexers = true:silent
38 | csharp_style_expression_bodied_accessors = true:silent
39 | csharp_style_expression_bodied_lambdas = true:silent
40 | csharp_style_expression_bodied_local_functions = false:silent
41 | csharp_style_conditional_delegate_call = true:suggestion
42 | csharp_style_var_for_built_in_types = false:silent
43 | csharp_style_var_when_type_is_apparent = false:silent
44 | csharp_style_var_elsewhere = false:silent
45 | csharp_prefer_simple_using_statement = true:suggestion
46 | csharp_prefer_braces = true:silent
47 | csharp_style_namespace_declarations = block_scoped:silent
48 | csharp_style_prefer_method_group_conversion = true:silent
49 | csharp_style_prefer_top_level_statements = true:silent
50 | csharp_style_prefer_primary_constructors = true:suggestion
51 |
52 | [*.cs]
53 | # 命名规则
54 |
55 | # Define the symbol group: private and internal fields
56 | dotnet_naming_symbols.private_internal_fields.applicable_kinds = field
57 | dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal
58 | dotnet_naming_symbols.private_internal_fields.required_modifiers =
59 |
60 | # Define the naming style: underscore prefix + camelCase
61 | dotnet_naming_style.underscore_camelcase_style.required_prefix = _
62 | dotnet_naming_style.underscore_camelcase_style.capitalization = camel_case
63 | dotnet_naming_style.underscore_camelcase_style.required_suffix =
64 |
65 | # Create a rule linking the symbols to the style
66 | dotnet_naming_rule.private_internal_fields_should_have_underscore.symbols = private_internal_fields
67 | dotnet_naming_rule.private_internal_fields_should_have_underscore.style = underscore_camelcase_style
68 | dotnet_naming_rule.private_internal_fields_should_have_underscore.severity = warning
69 | csharp_indent_labels = one_less_than_current
70 | csharp_prefer_system_threading_lock = true:suggestion
71 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | * text=auto
5 |
6 | ###############################################################################
7 | # Set default behavior for command prompt diff.
8 | #
9 | # This is need for earlier builds of msysgit that does not have it on by
10 | # default for csharp files.
11 | # Note: This is only used by command line
12 | ###############################################################################
13 | #*.cs diff=csharp
14 |
15 | ###############################################################################
16 | # Set the merge driver for project and solution files
17 | #
18 | # Merging from the command prompt will add diff markers to the files if there
19 | # are conflicts (Merging from VS is not affected by the settings below, in VS
20 | # the diff markers are never inserted). Diff markers may cause the following
21 | # file extensions to fail to load in VS. An alternative would be to treat
22 | # these files as binary and thus will always conflict and require user
23 | # intervention with every merge. To do so, just uncomment the entries below
24 | ###############################################################################
25 | #*.sln merge=binary
26 | #*.csproj merge=binary
27 | #*.vbproj merge=binary
28 | #*.vcxproj merge=binary
29 | #*.vcproj merge=binary
30 | #*.dbproj merge=binary
31 | #*.fsproj merge=binary
32 | #*.lsproj merge=binary
33 | #*.wixproj merge=binary
34 | #*.modelproj merge=binary
35 | #*.sqlproj merge=binary
36 | #*.wwaproj merge=binary
37 |
38 | ###############################################################################
39 | # behavior for image files
40 | #
41 | # image files are treated as binary by default.
42 | ###############################################################################
43 | #*.jpg binary
44 | #*.png binary
45 | #*.gif binary
46 |
47 | ###############################################################################
48 | # diff behavior for common document formats
49 | #
50 | # Convert binary document formats to text before diffing them. This feature
51 | # is only available from the command line. Turn it on by uncommenting the
52 | # entries below.
53 | ###############################################################################
54 | #*.doc diff=astextplain
55 | #*.DOC diff=astextplain
56 | #*.docx diff=astextplain
57 | #*.DOCX diff=astextplain
58 | #*.dot diff=astextplain
59 | #*.DOT diff=astextplain
60 | #*.pdf diff=astextplain
61 | #*.PDF diff=astextplain
62 | #*.rtf diff=astextplain
63 | #*.RTF diff=astextplain
64 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "DstDownloader"]
2 | path = DstDownloader
3 | url = https://github.com/ilyfairy/DstDownloader.git
4 | [submodule "PrettyPrompt"]
5 | path = PrettyPrompt
6 | url = https://github.com/ilyfairy/PrettyPrompt.git
7 | [submodule "GeoIP2-dotnet"]
8 | path = GeoIP2-dotnet
9 | url = https://github.com/ilyfairy/GeoIP2-dotnet
10 |
--------------------------------------------------------------------------------
/DstServerQuery.EntityFrameworkCore/DatabaseType.cs:
--------------------------------------------------------------------------------
1 | namespace DstServerQuery.EntityFrameworkCore;
2 |
3 | public enum DatabaseType
4 | {
5 | None = 0,
6 | SqlServer,
7 | MySql,
8 | Sqlite,
9 | PostgreSql,
10 | Memory,
11 | }
12 |
--------------------------------------------------------------------------------
/DstServerQuery.EntityFrameworkCore/DstServerQuery.EntityFrameworkCore.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net9.0
5 | enable
6 | enable
7 | DstServerQuery.EntityFrameworkCore
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/DstServerQuery.EntityFrameworkCore/Helpers/DateTimeOffsetUtcConverter.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
2 |
3 | namespace DstServerQuery.EntityFrameworkCore.Helpers;
4 |
5 | public class DateTimeOffsetUtcConverter : ValueConverter
6 | {
7 | public static ValueConverterInfo DefaultInfo { get; } = new ValueConverterInfo(typeof(DateTimeOffset), typeof(DateTimeOffset), (i) => new DateTimeOffsetUtcConverter(i.MappingHints));
8 |
9 | public DateTimeOffsetUtcConverter()
10 | : this(null)
11 | {
12 | }
13 |
14 | public DateTimeOffsetUtcConverter(ConverterMappingHints? mappingHints)
15 | : base((v) => ToUtc(v), (v) => ToLocal(v), mappingHints)
16 | {
17 | }
18 |
19 | public static DateTimeOffset ToLocal(DateTimeOffset v)
20 | {
21 | return v.ToLocalTime();
22 | }
23 |
24 | public static DateTimeOffset ToUtc(DateTimeOffset v)
25 | {
26 | return v.ToUniversalTime();
27 | }
28 | }
--------------------------------------------------------------------------------
/DstServerQuery.EntityFrameworkCore/HistoryCountService.cs:
--------------------------------------------------------------------------------
1 | using DstServerQuery.EntityFrameworkCore.Model;
2 | using DstServerQuery.EntityFrameworkCore.Model.Entities;
3 | using DstServerQuery.Models;
4 | using DstServerQuery.Models.Lobby;
5 | using DstServerQuery.Models.Requests;
6 | using Microsoft.EntityFrameworkCore;
7 | using Microsoft.Extensions.DependencyInjection;
8 | using Microsoft.Extensions.Logging;
9 |
10 | namespace DstServerQuery.EntityFrameworkCore;
11 |
12 | ///
13 | /// 大厅服务器历史房间数量管理器
14 | ///
15 | public class HistoryCountService
16 | {
17 | private readonly ILogger _logger;
18 | private readonly IServiceScopeFactory serviceScopeFactory;
19 | private readonly Queue cache = new(10100);
20 | private readonly bool isCountFromPlayers;
21 |
22 | public DateTimeOffset LastUpdate { get; private set; }
23 | public DateTimeOffset First => cache.FirstOrDefault()?.UpdateDate ?? DateTimeOffset.Now;
24 | public IEnumerable Cache => cache;
25 |
26 | public HistoryCountService(IServiceScopeFactory serviceScopeFactory, ILogger logger, DstWebConfig config)
27 | {
28 | _logger = logger;
29 | this.serviceScopeFactory = serviceScopeFactory;
30 | }
31 |
32 | ///
33 | /// 初始化,缓存3天数据
34 | ///
35 | public async Task Initialize()
36 | {
37 | using var scope = serviceScopeFactory.CreateScope();
38 | var dbContext = scope.ServiceProvider.GetRequiredService();
39 |
40 | var day3 = DateTimeOffset.Now - TimeSpan.FromDays(3); //三天前
41 | var r = await dbContext.ServerHistoryCountInfos.Where(v => v.UpdateDate > day3).AsNoTracking().ToArrayAsync();
42 | foreach (var item in r)
43 | {
44 | cache.Enqueue(item);
45 | }
46 | _logger.LogInformation("HistoryCountService 初始缓存个数:{CacheCount}", cache.Count);
47 | }
48 |
49 | private async Task AddAsync(ServerCountInfo info, CancellationToken cancellationToken)
50 | {
51 | using var scope = serviceScopeFactory.CreateScope();
52 | var dbContext = scope.ServiceProvider.GetRequiredService();
53 |
54 | var r = dbContext.ServerHistoryCountInfos.Add(info);
55 | await dbContext.SaveChangesAsync(cancellationToken);
56 |
57 | cache.Enqueue(info);
58 |
59 | while (cache.Count > 10000)
60 | {
61 | cache.Dequeue();
62 | }
63 |
64 | }
65 |
66 | // data to info
67 | public Task AddAsync(ICollection data, DateTimeOffset updateTime, CancellationToken cancellationToken)
68 | {
69 | LastUpdate = updateTime;
70 |
71 | ServerCountInfo countInfo = new();
72 | countInfo.UpdateDate = updateTime;
73 | countInfo.AllServerCount = data.Count;
74 |
75 | foreach (var item in data)
76 | {
77 | countInfo.AllPlayerCount += isCountFromPlayers ? item.Players?.Length ?? 0 : item.Connected;
78 | switch (item.Platform)
79 | {
80 | case Platform.Steam:
81 | countInfo.SteamServerCount++;
82 | countInfo.SteamPlayerCount += isCountFromPlayers ? item.Players?.Length ?? 0 : item.Connected;
83 | break;
84 | case Platform.PlayStation:
85 | countInfo.PlayStationServerCount++;
86 | countInfo.PlayStationPlayerCount += isCountFromPlayers ? item.Players?.Length ?? 0 : item.Connected;
87 | break;
88 | case Platform.WeGame or Platform.QQGame:
89 | countInfo.WeGameServerCount++;
90 | countInfo.WeGamePlayerCount += isCountFromPlayers ? item.Players?.Length ?? 0 : item.Connected;
91 | break;
92 | case Platform.Xbox:
93 | countInfo.XboxServerCount++;
94 | countInfo.XboxPlayerCount += isCountFromPlayers ? item.Players?.Length ?? 0 : item.Connected;
95 | break;
96 | case Platform.Switch:
97 | countInfo.SwitchServerCount++;
98 | countInfo.SwitchPlayerCount += isCountFromPlayers ? item.Players?.Length ?? 0 : item.Connected;
99 | break;
100 | }
101 | }
102 | return AddAsync(countInfo, cancellationToken);
103 | }
104 |
105 | ///
106 | /// 获取缓存的服务器历史数量信息
107 | ///
108 | ///
109 | public ServerCountInfo[] GetServerHistory()
110 | {
111 | return cache.ToArray();
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/DstServerQuery.EntityFrameworkCore/Model/DstDbContext.cs:
--------------------------------------------------------------------------------
1 | using DstServerQuery.EntityFrameworkCore.Helpers;
2 | using DstServerQuery.EntityFrameworkCore.Model.Entities;
3 | using EFCore.BulkExtensions;
4 | using Microsoft.EntityFrameworkCore;
5 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
6 |
7 | namespace DstServerQuery.EntityFrameworkCore.Model;
8 |
9 | public abstract class DstDbContext(DbContextOptions options) : DbContext(options)
10 | {
11 | public DbSet ServerHistoryCountInfos { get; set; }
12 | public DbSet Players { get; set; }
13 | public DbSet ServerHistories { get; set; }
14 | public DbSet ServerHistoryItems { get; set; }
15 | public DbSet HistoryServerItemPlayerPair { get; set; }
16 | public DbSet DaysInfos { get; set; }
17 | public DbSet TagColors { get; set; }
18 |
19 | protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
20 | {
21 | //全局禁用跟踪查询
22 | //optionsBuilder.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTrackingWithIdentityResolution);
23 | #if DEBUG
24 | //显示更详细的日志
25 | //optionsBuilder.EnableDetailedErrors();
26 | optionsBuilder.EnableSensitiveDataLogging();
27 | #endif
28 | }
29 |
30 | protected override void OnModelCreating(ModelBuilder modelBuilder)
31 | {
32 | var provider = Database.ProviderName ?? "";
33 | if (provider.Contains("sqlserver", StringComparison.OrdinalIgnoreCase))
34 | {
35 | modelBuilder.UseCollation("Chinese_PRC_BIN");
36 | }
37 | else if (provider.Contains("mysql", StringComparison.OrdinalIgnoreCase))
38 | {
39 | modelBuilder.UseCollation("utf8mb4_bin");
40 | }
41 | else if (provider.Contains("sqlite", StringComparison.OrdinalIgnoreCase))
42 | {
43 | modelBuilder.UseCollation("BINARY");
44 | foreach (var entityType in modelBuilder.Model.GetEntityTypes())
45 | {
46 | var properties = entityType.ClrType.GetProperties()
47 | .Where(p => p.PropertyType == typeof(DateTimeOffset) || p.PropertyType == typeof(DateTimeOffset?));
48 | foreach (var property in properties)
49 | {
50 | modelBuilder
51 | .Entity(entityType.Name)
52 | .Property(property.Name)
53 | .HasConversion(new DateTimeOffsetToBinaryConverter());
54 | }
55 | }
56 | }
57 | else if (provider.Contains("postgresql", StringComparison.OrdinalIgnoreCase))
58 | {
59 | //modelBuilder.UseCollation("C");
60 | foreach (var entityType in modelBuilder.Model.GetEntityTypes())
61 | {
62 | var properties = entityType.ClrType.GetProperties()
63 | .Where(p => p.PropertyType == typeof(DateTimeOffset) || p.PropertyType == typeof(DateTimeOffset?));
64 | foreach (var property in properties)
65 | {
66 | modelBuilder
67 | .Entity(entityType.Name)
68 | .Property(property.Name)
69 | .HasConversion(new DateTimeOffsetUtcConverter());
70 | }
71 | }
72 | }
73 |
74 | //服务器信息和历史记录信息的一对多
75 | modelBuilder.Entity()
76 | .HasMany(v => v.Items)
77 | .WithOne(v => v.Server)
78 | .HasForeignKey(v => v.ServerId)
79 | .IsRequired();
80 |
81 | //历史记录信息和天数信息的一对一
82 | modelBuilder.Entity()
83 | .HasOne(v => v.DaysInfo)
84 | .WithOne(v => v.ServerItem)
85 | .HasForeignKey(v => v.DaysInfoId);
86 |
87 | //历史记录信息和玩家信息多对多
88 | modelBuilder.Entity()
89 | .HasMany(v => v.Players)
90 | .WithMany(v => v.ServerHistoryItems)
91 | .UsingEntity( // OnDelete禁用联级删除
92 | l => l.HasOne(v => v.Player).WithMany().HasForeignKey(v => v.PlayerId).OnDelete(DeleteBehavior.Restrict),
93 | r => r.HasOne(v => v.HistoryServerItem).WithMany().HasForeignKey(v => v.HistoryServerItemId).OnDelete(DeleteBehavior.Restrict)
94 | );
95 | }
96 |
97 |
98 | public async Task GetTagColorAsync(string name)
99 | {
100 | var color = await TagColors.AsNoTracking()
101 | .FirstOrDefaultAsync(v => v.Name == name);
102 | return color?.Color;
103 | }
104 |
105 | public async Task SetTagColorAsync(string name, string color)
106 | {
107 | var model = await TagColors.AsNoTracking()
108 | .FirstOrDefaultAsync(v => v.Name == name);
109 |
110 | if (model == null)
111 | {
112 | TagColors.Add(new TagColorItem() { Name = name, Color = color });
113 | await SaveChangesAsync();
114 | }
115 | else
116 | {
117 | model.Color = color;
118 | Update(model);
119 | await SaveChangesAsync();
120 | }
121 | }
122 |
123 | public async Task SetTagColorAsync(IEnumerable> colors)
124 | {
125 | await this.BulkInsertOrUpdateAsync(colors.Select(v =>
126 | {
127 | return new TagColorItem() { Name = v.Key, Color = v.Value };
128 | }));
129 | }
130 | }
131 |
132 | public class MemoryDstDbContext(DbContextOptions dbContextOptions) : DstDbContext(dbContextOptions);
133 |
134 | public class SqliteDstDbContext(DbContextOptions dbContextOptions) : DstDbContext(dbContextOptions);
135 | public class MySqlDstDbContext(DbContextOptions dbContextOptions) : DstDbContext(dbContextOptions);
136 | public class SqlServerDstDbContext(DbContextOptions dbContextOptions) : DstDbContext(dbContextOptions);
137 | public class PostgreSqlDstDbContext(DbContextOptions dbContextOptions) : DstDbContext(dbContextOptions);
--------------------------------------------------------------------------------
/DstServerQuery.EntityFrameworkCore/Model/Entities/ColorItem.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using System.ComponentModel.DataAnnotations;
3 |
4 | namespace DstServerQuery.EntityFrameworkCore.Model.Entities;
5 |
6 | [Index(nameof(Name), IsUnique = true)]
7 | public abstract class ColorItem
8 | {
9 | [Required, Key]
10 | public required string Name { get; set; }
11 |
12 | [Required]
13 | public required string Color { get; set; }
14 | }
15 |
16 | public class TagColorItem : ColorItem;
--------------------------------------------------------------------------------
/DstServerQuery.EntityFrameworkCore/Model/Entities/DstPlayer.cs:
--------------------------------------------------------------------------------
1 | using DstServerQuery.Models;
2 | using Microsoft.EntityFrameworkCore;
3 | using System.ComponentModel.DataAnnotations;
4 | using System.ComponentModel.DataAnnotations.Schema;
5 | using System.Text.Json.Serialization;
6 |
7 | namespace DstServerQuery.EntityFrameworkCore.Model.Entities;
8 |
9 | ///
10 | /// 服务器玩家
11 | ///
12 | [Index(nameof(Name), nameof(Platform))]
13 | public class DstPlayer
14 | {
15 | [Key, DatabaseGenerated(DatabaseGeneratedOption.None)]
16 | public required string Id { get; set; }
17 | public required string Name { get; set; }
18 | public Platform Platform { get; set; }
19 |
20 | ///
21 | /// 这个玩家存在哪些服务器中存在过
22 | ///
23 | [JsonIgnore]
24 | public ICollection ServerHistoryItems { get; set; } = [];
25 | }
26 |
--------------------------------------------------------------------------------
/DstServerQuery.EntityFrameworkCore/Model/Entities/DstServerHistory.cs:
--------------------------------------------------------------------------------
1 | using DstServerQuery.Models;
2 | using Microsoft.EntityFrameworkCore;
3 | using System.ComponentModel.DataAnnotations;
4 | using System.ComponentModel.DataAnnotations.Schema;
5 | using System.Diagnostics.CodeAnalysis;
6 | using System.Text.Json.Serialization;
7 |
8 | namespace DstServerQuery.EntityFrameworkCore.Model.Entities;
9 |
10 | ///
11 | /// 服务器, 保存了几乎不可变的字段
12 | ///
13 | [Index(nameof(Id), nameof(UpdateTime), nameof(Name))]
14 | public class DstServerHistory
15 | {
16 | ///
17 | /// 主键
RowId
18 | ///
19 | [Key, DatabaseGenerated(DatabaseGeneratedOption.None)]
20 | public required string Id { get; set; } = null!;
21 |
22 | public string Name { get; set; }
23 | public string IP { get; set; }
24 | public int Port { get; set; }
25 | public string Host { get; set; }
26 | public DateTimeOffset UpdateTime { get; set; }
27 | public Platform Platform { get; set; }
28 | public string? GameMode { get; set; }
29 | public string? Intent { get; set; }
30 |
31 | [JsonIgnore]
32 | public ICollection Items { get; set; } = [];
33 | }
34 |
35 |
36 | [Index(nameof(Id), nameof(DateTime))]
37 | public class DstServerHistoryItem
38 | {
39 | [Key]
40 | public long Id { get; set; }
41 |
42 | public string? Season { get; set; }
43 | public int PlayerCount { get; set; }
44 |
45 | public DateTimeOffset DateTime { get; set; }
46 |
47 | public string ServerId { get; set; }
48 | public DstServerHistory Server { get; set; }
49 |
50 |
51 | public bool IsDetailed { get; set; }
52 | public DstDaysInfo? DaysInfo { get; set; }
53 | public long? DaysInfoId { get; set; }
54 | public ICollection? Players { get; set; }
55 |
56 | public int GetPlayerCount() => Players?.Count ?? PlayerCount;
57 | }
58 |
59 | public class DstDaysInfo
60 | {
61 | [Key]
62 | [JsonIgnore]
63 | public long Id { get; set; }
64 |
65 | ///
66 | /// 当前天数
67 | ///
68 | public int Day { get; set; }
69 |
70 | ///
71 | /// 当前季节已过去天数
72 | ///
73 | public int DaysElapsedInSeason { get; set; }
74 |
75 | ///
76 | /// 当前季节剩余天数
77 | ///
78 | public int DaysLeftInSeason { get; set; }
79 |
80 | public int TotalDaysSeason => DaysElapsedInSeason + DaysLeftInSeason;
81 |
82 | [JsonIgnore]
83 | public DstServerHistoryItem ServerItem { get; set; } = null!;
84 |
85 | [return: NotNullIfNotNull(nameof(lobbyDaysInfo))]
86 | public static DstDaysInfo? FromLobby(LobbyDaysInfo? lobbyDaysInfo)
87 | => lobbyDaysInfo is null ? null : new() { Day = lobbyDaysInfo.Day, DaysElapsedInSeason = lobbyDaysInfo.DaysElapsedInSeason, DaysLeftInSeason = lobbyDaysInfo.DaysLeftInSeason };
88 | }
89 |
90 |
91 | public class HistoryServerItemPlayer
92 | {
93 | public DstPlayer Player { get; set; }
94 | public string PlayerId { get; set; }
95 |
96 | public DstServerHistoryItem HistoryServerItem { get; set; }
97 | public long HistoryServerItemId { get; set; }
98 | }
--------------------------------------------------------------------------------
/DstServerQuery.EntityFrameworkCore/Model/Entities/ServerCountInfo.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 | using System.Text.Json.Serialization;
3 |
4 | namespace DstServerQuery.EntityFrameworkCore.Model.Entities;
5 |
6 | ///
7 | /// 服务器数量信息
8 | ///
9 | public record ServerCountInfo
10 | {
11 | [Key, JsonIgnore]
12 | public int Id { get; set; }
13 |
14 | //`(*>﹏<*)′=================================== 一条华丽的分割线 ===================================`(*>﹏<*)′//
15 | public DateTimeOffset UpdateDate { get; set; }
16 | public int AllServerCount { get; set; }
17 | public int AllPlayerCount { get; set; }
18 | //`(*>﹏<*)′=================================== 一条华丽的分割线 ===================================`(*>﹏<*)′//
19 | public int SteamServerCount { get; set; }
20 | public int WeGameServerCount { get; set; }
21 | public int PlayStationServerCount { get; set; }
22 | public int XboxServerCount { get; set; }
23 | public int? SwitchServerCount { get; set; }
24 | //`(*>﹏<*)′=================================== 一条华丽的分割线 ===================================`(*>﹏<*)′//
25 | public int SteamPlayerCount { get; set; }
26 | public int WeGamePlayerCount { get; set; }
27 | public int PlayStationPlayerCount { get; set; }
28 | public int XboxPlayerCount { get; set; }
29 | public int? SwitchPlayerCount { get; set; }
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/DstServerQuery.EntityFrameworkCore/Model/SimpleCacheDatabase.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using System.ComponentModel.DataAnnotations;
3 | using System.Text.Json;
4 | using System.Text.Json.Nodes;
5 | using System.Text.Json.Serialization;
6 |
7 | namespace DstServerQuery.EntityFrameworkCore.Model;
8 |
9 | public class SimpleCacheDatabase : DbContext
10 | {
11 | public DbSet Items { get; set; }
12 |
13 | public JsonSerializerOptions SerializerOptions { get; set; } = new()
14 | {
15 | NumberHandling = JsonNumberHandling.AllowReadingFromString,
16 | ReferenceHandler = ReferenceHandler.Preserve,
17 | };
18 |
19 | public SimpleCacheDatabase(DbContextOptions dbContextOptions) : base(dbContextOptions)
20 | {
21 | }
22 |
23 | public JsonNode? this[string key]
24 | {
25 | get
26 | {
27 | var item = Items.AsNoTracking().FirstOrDefault(v => v.Id == key);
28 | if (item is null || item.Data is null) return default;
29 | return JsonNode.Parse(item.Data);
30 | }
31 | set
32 | {
33 | var data = JsonSerializer.Serialize(value, SerializerOptions);
34 | var item = Items.FirstOrDefault(v => v.Id == key);
35 | if (item is null)
36 | {
37 | Items.Add(new DataItem() { Id = key, Data = data });
38 | }
39 | else
40 | {
41 | item.Data = data;
42 | Items.Update(item);
43 | }
44 | SaveChanges();
45 | }
46 | }
47 |
48 | public void EnsureInitialize()
49 | {
50 | Database.EnsureCreated();
51 | }
52 |
53 | public T? Get(string key)
54 | {
55 | var item = Items.AsNoTracking().FirstOrDefault(v => v.Id == key);
56 | if (item is null || item.Data is null) return default;
57 | return JsonSerializer.Deserialize(item.Data, SerializerOptions);
58 | }
59 |
60 | public void Set(string key, T value)
61 | {
62 | var item = Items.FirstOrDefault(v => v.Id == key);
63 | var data = JsonSerializer.Serialize(value, SerializerOptions);
64 | if (item is null)
65 | {
66 | Items.Add(new DataItem() { Id = key, Data = data });
67 | }
68 | else
69 | {
70 | item.Data = data;
71 | Items.Update(item);
72 | }
73 | SaveChanges();
74 | }
75 |
76 |
77 |
78 | public class DataItem
79 | {
80 | [Key]
81 | public required string Id { get; set; }
82 |
83 | public string? Data { get; set; }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/DstServerQuery.Web/.config/dotnet-tools.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 1,
3 | "isRoot": true,
4 | "tools": {
5 | "dotnet-ef": {
6 | "version": "8.0.0",
7 | "commands": [
8 | "dotnet-ef"
9 | ]
10 | }
11 | }
12 | }
--------------------------------------------------------------------------------
/DstServerQuery.Web/ConfigureSwaggerOptions.cs:
--------------------------------------------------------------------------------
1 | using Asp.Versioning.ApiExplorer;
2 | using Microsoft.Extensions.Options;
3 | using Microsoft.OpenApi.Models;
4 | using Swashbuckle.AspNetCore.SwaggerGen;
5 |
6 | namespace DstServerQuery.Web;
7 |
8 | public class ConfigureSwaggerOptions : IConfigureOptions
9 | {
10 | readonly IApiVersionDescriptionProvider provider;
11 |
12 | public ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider) => this.provider = provider;
13 |
14 | public void Configure(SwaggerGenOptions options)
15 | {
16 | foreach (var description in provider.ApiVersionDescriptions)
17 | {
18 | options.SwaggerDoc(
19 | description.GroupName,
20 | new OpenApiInfo()
21 | {
22 | Title = $"API {description.ApiVersion}",
23 | Version = description.ApiVersion.ToString(),
24 | });
25 | }
26 | }
27 | }
--------------------------------------------------------------------------------
/DstServerQuery.Web/Controllers/V2/HelperController.cs:
--------------------------------------------------------------------------------
1 | using Asp.Versioning;
2 | using DstServerQuery.EntityFrameworkCore.Model;
3 | using DstServerQuery.Web.Helpers;
4 | using DstServerQuery.Web.Models.Http;
5 | using Microsoft.AspNetCore.Mvc;
6 | using Microsoft.AspNetCore.RateLimiting;
7 | using Microsoft.EntityFrameworkCore;
8 |
9 | namespace DstServerQuery.Web.Controllers.V2;
10 |
11 | [ApiController]
12 | [ApiVersion(2.0)]
13 | [Route("api/v{version:apiVersion}/[controller]")]
14 | [Produces("application/json")]
15 | [EnableRateLimiting("fixed")]
16 | public class HelperController(
17 | ILogger _logger,
18 | DstDbContext _dbContext
19 | ) : ControllerBase
20 | {
21 | ///
22 | /// 根据标签获取颜色
23 | ///
24 | ///
25 | ///
26 | [HttpPost("GetTagsColor")]
27 | [Produces("application/json")]
28 | [ProducesResponseType(200)]
29 | [ApiExplorerSettings(IgnoreApi = true)]
30 | public async Task GetTagsColor([FromBody] string[] tags)
31 | {
32 | var colors = await _dbContext.TagColors.Where(v => tags.Contains(v.Name)).ToArrayAsync();
33 |
34 | Dictionary colorDictionary = [];
35 | foreach (var color in colors)
36 | {
37 | colorDictionary.Add(color.Name, color.Name);
38 | }
39 |
40 | foreach (var item in tags.Except(colors.Select(v => v.Name)))
41 | {
42 | colorDictionary.Add(item, Helper.GetRandomColor(50, 180));
43 | }
44 |
45 | ColorResponse response = new()
46 | {
47 | Colors = colorDictionary
48 | };
49 | return response.ToJsonResult();
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/DstServerQuery.Web/DstServerQuery.Web.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net9.0
5 | enable
6 | enable
7 | 66626388-8029-405a-a20c-4f45f8782739
8 | DstServerQuery.Web
9 | false
10 | preview
11 | true
12 | $(NoWarn);1591
13 |
14 |
15 |
16 |
17 |
18 | all
19 | runtime; build; native; contentfiles; analyzers; buildtransitive
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | PreserveNewest
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/DstServerQuery.Web/DstServerQuery.Web.http:
--------------------------------------------------------------------------------
1 | @DstServerQuery.Web_HostAddress = http://127.0.0.1:3000
2 | @ApiVersion = 2
3 |
4 | //获取饥荒版本
5 | POST {{DstServerQuery.Web_HostAddress}}/api/v{{ApiVersion}}/server/Version
6 |
7 | ###
8 |
9 | //请求列表
10 | POST {{DstServerQuery.Web_HostAddress}}/api/v{{ApiVersion}}/server/list
11 | Content-Type: application/json
12 |
13 | {
14 |
15 | }
16 | ###
--------------------------------------------------------------------------------
/DstServerQuery.Web/GeoLite2-City.mmdb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ilyfairy/DstServerQuery/92c5801f224164344d880ccb2580a052ba3ee91b/DstServerQuery.Web/GeoLite2-City.mmdb
--------------------------------------------------------------------------------
/DstServerQuery.Web/Helpers/Commands/AppcalitionCommand.cs:
--------------------------------------------------------------------------------
1 | using System.CommandLine.Builder;
2 | using System.CommandLine.Parsing;
3 | using System.Text.Json.Nodes;
4 | using Spectre.Console.Json;
5 | using System.CommandLine;
6 | using Spectre.Console;
7 | using System.Diagnostics.CodeAnalysis;
8 |
9 | namespace DstServerQuery.Web.Helpers.Commands;
10 |
11 | public class AppcalitionCommand : RootCommand
12 | {
13 | public Parser? Parser { get; set; }
14 | public bool IsExit { get; set; }
15 |
16 | public AppcalitionCommand(IServiceProvider serviceProvider)
17 | {
18 | AddClear();
19 | AddStop();
20 | AddCommand(new GCCommand());
21 | AddCommand(new DstCommand(serviceProvider));
22 | }
23 |
24 | [MemberNotNull(nameof(Parser))]
25 | public void BuildParser()
26 | {
27 | Parser = new CommandLineBuilder(this)
28 | .UseHelp("help")
29 | .AddMiddleware(v =>
30 | {
31 | if (v.ParseResult.Errors.Count > 0)
32 | {
33 | v.ExitCode = 0xffff;
34 | if (v.ParseResult.Tokens.Count != 0)
35 | {
36 | if (v.ParseResult.UnmatchedTokens.Count > 0)
37 | {
38 | AnsiConsole.MarkupLine("[red]{0}[/]", Markup.Escape($"无法识别的命令或参数: {v.ParseResult.UnmatchedTokens[0]}"));
39 | }
40 | else
41 | {
42 | AnsiConsole.MarkupLine("[red]缺少参数[/]");
43 | }
44 | }
45 |
46 | foreach (var target in v.ParseResult.CommandResult.Command.GetCompletions())
47 | {
48 | AnsiConsole.MarkupLine($" [blue]{Markup.Escape(target.Label)}[/] {Markup.Escape(target.Detail ?? "")}");
49 | }
50 | }
51 | //v.InvocationResult = null;
52 | })
53 | .UseExceptionHandler((exception, context) =>
54 | {
55 | if (context.ExitCode == 0xffff)
56 | {
57 | return;
58 | }
59 | AnsiConsole.Write(new Panel(Markup.Escape(exception.ToString()))
60 | {
61 | Header = new PanelHeader("命令执行异常"),
62 | BorderStyle = new Style(Color.Red),
63 | Expand = true
64 | });
65 | })
66 | .Build();
67 | }
68 |
69 | public void AddClear()
70 | {
71 | var clearCommand = new Command("clear", "清空控制台");
72 | clearCommand.SetHandler(() =>
73 | {
74 | AnsiConsole.Clear();
75 | });
76 |
77 | AddCommand(clearCommand);
78 | }
79 |
80 | public void AddStop()
81 | {
82 | var exitCommand = new Command("stop", "退出");
83 | exitCommand.AddAlias("exit");
84 | exitCommand.AddAlias("quit");
85 | exitCommand.SetHandler(() =>
86 | {
87 | IsExit = true;
88 | });
89 |
90 | AddCommand(exitCommand);
91 | }
92 |
93 | public void AddTest()
94 | {
95 | string testJson = """
96 | {
97 | "type": "object",
98 | "properties": {
99 | "name": {
100 | "type": "string"
101 | },
102 | "age": {
103 | "type": "number"
104 | },
105 | "address": {
106 | "type": "object"
107 | },
108 | "phoneNumbers": {
109 | "type": "array",
110 | "items": {
111 | "type": "object",
112 | "properties": {
113 | "type": {
114 | "type": "string"
115 | },
116 | "number": {
117 | "type": "string"
118 | }
119 | },
120 | "additionalProperties": false
121 | }
122 | }
123 | },
124 | "additionalProperties": false
125 | }
126 | """;
127 |
128 | var ex = new Command("ex", "触发一个异常");
129 | ex.SetHandler(() =>
130 | {
131 | throw new Exception("异常了");
132 | });
133 | AddCommand(ex);
134 |
135 | var jsonCommand = new Command("json", "输出json");
136 | jsonCommand.SetHandler(() =>
137 | {
138 | AnsiConsole.Write(new Panel(new JsonText(testJson)) { Header = new("测试Json") });
139 | });
140 | AddCommand(jsonCommand);
141 |
142 | var bingimgCommand = new Command("img", "获取bing每日壁纸");
143 | HttpClient http = new();
144 | bingimgCommand.SetHandler(async () =>
145 | {
146 | try
147 | {
148 | CanvasImage? image = null;
149 | Stream? imageStream = null;
150 | await AnsiConsole.Status()
151 | .Spinner(Spinner.Known.Weather)
152 | .AutoRefresh(true)
153 | .StartAsync("正在获取", async v =>
154 | {
155 | var json = await http.GetStringAsync("https://www.bing.com/HPImageArchive.aspx?format=js&idx=0&n=1&mkt=zh-CN");
156 | var subPath = JsonNode.Parse(json)!["images"]![0]!["url"]!.ToString();
157 | var imgUrl = new Uri(new Uri("https://www.bing.com/"), subPath);
158 | imageStream = await http.GetStreamAsync(imgUrl);
159 | image = new CanvasImage(imageStream);
160 | });
161 | _ = image ?? throw new Exception("获取失败");
162 | AnsiConsole.Write(new Panel(image) { Header = new("Bing每日壁纸", Justify.Center) });
163 | }
164 | catch (Exception e)
165 | {
166 | AnsiConsole.Write(new Panel(e.Message)
167 | {
168 | Header = new("图片获取失败"),
169 | BorderStyle = new(Color.Red)
170 | });
171 | }
172 | });
173 | AddCommand(bingimgCommand);
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/DstServerQuery.Web/Helpers/Commands/CommandPromptCallbacks.cs:
--------------------------------------------------------------------------------
1 | using System.CommandLine.Parsing;
2 | using PrettyPrompt.Completion;
3 | using PrettyPrompt.Consoles;
4 | using PrettyPrompt.Documents;
5 | using PrettyPrompt.Highlighting;
6 | using System.CommandLine;
7 | using PrettyPrompt;
8 | using Spectre.Console;
9 | using DstServerQuery.Web.Helpers.Console;
10 |
11 | namespace DstServerQuery.Web.Helpers.Commands;
12 |
13 | public class CommandPromptCallbacks(Command rootCommand, ControllableConsoleSink controllableConsoleSink) : PromptCallbacks
14 | {
15 | private bool isCompletionPaneOpen;
16 | private string lastInput = "";
17 |
18 | private void CheckConsoleSink()
19 | {
20 | controllableConsoleSink.Enabled = string.IsNullOrWhiteSpace(lastInput) && isCompletionPaneOpen is false;
21 | }
22 |
23 | ///
24 | /// 把CommandLine的补全提供给PrettyPrompt
25 | ///
26 | ///
27 | ///
28 | ///
29 | ///
30 | ///
31 | protected override Task> GetCompletionItemsAsync(string text, int caret, TextSpan spanToBeReplaced, CancellationToken cancellationToken)
32 | {
33 | var r = rootCommand.Parse(text);
34 | var completions = r.GetCompletions();
35 |
36 | IReadOnlyList result = completions.Select(v =>
37 | {
38 | var label = v.Label;
39 | FormattedString displayText = "";
40 | if (v.Kind == "Keyword")
41 | {
42 | displayText = new FormattedString(label, new ConsoleFormat(AnsiColor.Rgb(0x3d, 0x9c, 0xd6)));
43 | }
44 |
45 | var item = new CompletionItem(
46 | label,
47 | displayText,
48 | null,
49 | _ => Task.FromResult(new FormattedString(v.Detail))
50 | );
51 |
52 | return item;
53 | }).ToArray();
54 |
55 | return Task.FromResult(result);
56 | }
57 |
58 | protected override Task ConfirmCompletionCommit(string text, int caret, KeyPress keyPress, CancellationToken cancellationToken)
59 | {
60 | return base.ConfirmCompletionCommit(text, caret, keyPress, cancellationToken);
61 | }
62 |
63 | //当有新输入时
64 | protected override Task<(string Text, int Caret)> FormatInput(string text, int caret, KeyPress keyPress, CancellationToken cancellationToken)
65 | {
66 | lastInput = text;
67 | CheckConsoleSink();
68 |
69 | return base.FormatInput(text, caret, keyPress, cancellationToken);
70 | }
71 |
72 |
73 | protected override Task CompletionPaneWindowStateChanged(bool isOpen)
74 | {
75 | //当补全窗口打开时,禁用日志输出
76 | isCompletionPaneOpen = isOpen;
77 | CheckConsoleSink();
78 |
79 | return Task.CompletedTask;
80 | }
81 |
82 |
83 |
84 | protected override Task ShouldOpenCompletionWindowAsync(string text, int caret, KeyPress keyPress, CancellationToken cancellationToken)
85 | {
86 | return base.ShouldOpenCompletionWindowAsync(text, caret, keyPress, cancellationToken);
87 | }
88 |
89 | protected override Task GetSpanToReplaceByCompletionAsync(string text, int caret, CancellationToken cancellationToken)
90 | {
91 | return base.GetSpanToReplaceByCompletionAsync(text, caret, cancellationToken);
92 | }
93 |
94 | protected override IEnumerable<(KeyPressPattern Pattern, KeyPressCallbackAsync Callback)> GetKeyPressCallbacks()
95 | {
96 | return base.GetKeyPressCallbacks();
97 | }
98 |
99 | protected override Task<(IReadOnlyList, int ArgumentIndex)> GetOverloadsAsync(string text, int caret, CancellationToken cancellationToken)
100 | {
101 | return base.GetOverloadsAsync(text, caret, cancellationToken);
102 | }
103 |
104 | protected override Task> HighlightCallbackAsync(string text, CancellationToken cancellationToken)
105 | {
106 | return base.HighlightCallbackAsync(text, cancellationToken);
107 | }
108 |
109 | protected override Task TransformKeyPressAsync(string text, int caret, KeyPress keyPress, CancellationToken cancellationToken)
110 | {
111 | return base.TransformKeyPressAsync(text, caret, keyPress, cancellationToken);
112 | }
113 | }
--------------------------------------------------------------------------------
/DstServerQuery.Web/Helpers/Commands/DstCommand.cs:
--------------------------------------------------------------------------------
1 | using DstServerQuery.Services;
2 | using Spectre.Console;
3 | using System.CommandLine;
4 |
5 | namespace DstServerQuery.Web.Helpers.Commands;
6 |
7 | public class DstCommand : Command
8 | {
9 | private DstVersionService _versionService;
10 |
11 | public DstCommand(IServiceProvider serviceProvider) : base("dst", "饥荒服务器查询")
12 | {
13 | _versionService = serviceProvider.GetRequiredService();
14 | AddVersion();
15 | }
16 |
17 | public void AddVersion()
18 | {
19 | Command versionCommand = new("version", "获取程序版本信息");
20 | versionCommand.SetHandler(() =>
21 | {
22 | AnsiConsole.WriteLine($"当前的版本: {_versionService.Version?.ToString() ?? "获取失败"}");
23 | });
24 | AddCommand(versionCommand);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/DstServerQuery.Web/Helpers/Commands/GCCommand.cs:
--------------------------------------------------------------------------------
1 | using Spectre.Console;
2 | using System.CommandLine;
3 | using System.Diagnostics;
4 | using System.Runtime.CompilerServices;
5 |
6 | namespace DstServerQuery.Web.Helpers.Commands;
7 |
8 | public class GCCommand : Command
9 | {
10 | private static TimeSpan startTime;
11 |
12 | [ModuleInitializer]
13 | public static void Initialize()
14 | {
15 | startTime = TimeSpan.FromMilliseconds(Environment.TickCount64);
16 | }
17 |
18 | public GCCommand() : base("gc", "垃圾回收器命令")
19 | {
20 | AddStatus();
21 | AddCollect();
22 | }
23 |
24 |
25 | public void AddStatus()
26 | {
27 | Command statusCommand = new("status", "获取垃圾回收器状态");
28 | statusCommand.SetHandler(() =>
29 | {
30 | var runningTime = TimeSpan.FromMilliseconds(Environment.TickCount64) - startTime;
31 | AnsiConsole.Write(
32 | new Panel(new Markup($"""
33 | 总运行时间: [blue]{runningTime:d\:hh\:mm\:ss}[/]
34 | 第0代垃圾回收次数: [blue]{GC.CollectionCount(0)}[/]
35 | 第1代垃圾回收次数: [blue]{GC.CollectionCount(1)}[/]
36 | 第2代垃圾回收次数: [blue]{GC.CollectionCount(2)}[/]
37 | GC总暂停时间: [blue]{GC.GetTotalPauseDuration().TotalSeconds:0.000}s[/]
38 | GC堆大小: [blue]{GC.GetTotalMemory(false) / 1024.0 / 1024.0:0.00}MB[/]
39 | 进程占用内存: [blue]{Process.GetCurrentProcess().PrivateMemorySize64 / 1024 / 1024:0.00}MB[/]
40 | """))
41 | {
42 | Header = new PanelHeader("垃圾回收器状态"),
43 | BorderStyle = new Style(Color.Yellow3)
44 | });
45 | });
46 |
47 | AddCommand(statusCommand);
48 | }
49 |
50 | public void AddCollect()
51 | {
52 | Command collectCommand = new("collect", "执行垃圾回收");
53 | Option g = new("--generation", "回收代系");
54 | g.SetDefaultValue("all");
55 | g.AddAlias("-g");
56 | g.AddCompletions("0", "1", "2", "all");
57 | g.AddValidator(v =>
58 | {
59 | var value = v.GetValueOrDefault()?.ToString();
60 | if (string.Equals(value, "all", StringComparison.OrdinalIgnoreCase))
61 | {
62 | return;
63 | }
64 | if (!int.TryParse(value, out int c) || c is < 0 or > 2)
65 | {
66 | v.ErrorMessage = "参数只能是 all 或 0~2";
67 | }
68 | return;
69 | });
70 | collectCommand.AddOption(g);
71 | collectCommand.SetHandler(v =>
72 | {
73 | if (string.Equals(v, "all", StringComparison.OrdinalIgnoreCase))
74 | {
75 | GC.Collect();
76 | System.Console.WriteLine("已完成所有代系垃圾回收");
77 | return;
78 | }
79 | else
80 | {
81 | var c = int.Parse(v);
82 | GC.Collect();
83 | System.Console.WriteLine($"已完成第{c}代垃圾回收");
84 | }
85 | }, g);
86 |
87 | AddCommand(collectCommand);
88 | }
89 |
90 | }
91 |
--------------------------------------------------------------------------------
/DstServerQuery.Web/Helpers/Console/ControllableConsoleSink.cs:
--------------------------------------------------------------------------------
1 | using Serilog.Core;
2 | using Serilog.Events;
3 | using Serilog.Formatting;
4 | using Serilog.Sinks.SystemConsole.Themes;
5 | using System.Collections.Concurrent;
6 | using System.Reflection;
7 |
8 | namespace DstServerQuery.Web.Helpers.Console;
9 |
10 | public class ControllableConsoleSink : ILogEventSink
11 | {
12 | private readonly LogEventLevel? _standardErrorFromLevel;
13 | private readonly ITextFormatter _formatter;
14 | private readonly object _syncRoot = new();
15 | private const int DefaultWriteBufferCapacity = 256;
16 |
17 | public bool Enabled { get; set; } = true;
18 | public int HistoryLineMax { get; set; } = 1000;
19 | public ConcurrentQueue History { get; } = new();
20 |
21 | static ControllableConsoleSink()
22 | {
23 | try
24 | {
25 | Assembly.Load("Serilog.Sinks.Console").GetType("Serilog.Sinks.SystemConsole.Platform.WindowsConsole")!.GetMethod("EnableVirtualTerminalProcessing")!.Invoke(null, null);
26 | }
27 | catch (Exception) { }
28 | //WindowsConsole.EnableVirtualTerminalProcessing();
29 | }
30 |
31 | public ControllableConsoleSink(ITextFormatter formatter, LogEventLevel? standardErrorFromLevel)
32 | {
33 | _standardErrorFromLevel = standardErrorFromLevel;
34 | _formatter = formatter;
35 | }
36 |
37 | public void Emit(LogEvent logEvent)
38 | {
39 | History.Enqueue(logEvent);
40 | if (History.Count > HistoryLineMax)
41 | {
42 | History.TryDequeue(out _);
43 | }
44 |
45 | if (!Enabled)
46 | {
47 | return;
48 | }
49 | TextWriter textWriter = SelectOutputStream(logEvent.Level);
50 | lock (_syncRoot)
51 | {
52 | _formatter.Format(logEvent, textWriter);
53 | textWriter.Flush();
54 | }
55 | }
56 |
57 | private TextWriter SelectOutputStream(LogEventLevel logEventLevel)
58 | {
59 | LogEventLevel? standardErrorFromLevel = _standardErrorFromLevel;
60 | if (!standardErrorFromLevel.HasValue)
61 | {
62 | return System.Console.Out;
63 | }
64 |
65 | if (!(logEventLevel < _standardErrorFromLevel))
66 | {
67 | return System.Console.Error;
68 | }
69 |
70 | return System.Console.Out;
71 | }
72 |
73 |
74 | public static ControllableConsoleSink Create(string outputTemplate = "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}",
75 | IFormatProvider? formatProvider = null,
76 | LogEventLevel? standardErrorFromLevel = null)
77 | {
78 | ArgumentNullException.ThrowIfNull(outputTemplate);
79 |
80 | var OutputTemplateRendererType = Assembly.Load("Serilog.Sinks.Console").GetType("Serilog.Sinks.SystemConsole.Output.OutputTemplateRenderer")!; // .GetConstructors().First(v=>v.GetParameters().Length == 3);
81 | ConsoleTheme theme;
82 | try
83 | {
84 | theme = (ConsoleTheme)Assembly.Load("Serilog.Sinks.Console").GetType("Serilog.Sinks.SystemConsole.Themes.SystemConsoleThemes")!.GetProperty("Literate")!.GetValue(null)!;
85 | }
86 | catch (Exception)
87 | {
88 | theme = ConsoleTheme.None;
89 | }
90 |
91 | ITextFormatter formatter = (ITextFormatter)Activator.CreateInstance(OutputTemplateRendererType, [theme, outputTemplate, formatProvider])!;
92 | return new ControllableConsoleSink(formatter, standardErrorFromLevel);
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/DstServerQuery.Web/Helpers/DstPlayerEqualityComparer.cs:
--------------------------------------------------------------------------------
1 | using DstServerQuery.EntityFrameworkCore.Model.Entities;
2 | using System.Diagnostics.CodeAnalysis;
3 |
4 | namespace DstServerQuery.Web.Helpers;
5 |
6 | public class DstPlayerEqualityComparer : IEqualityComparer
7 | {
8 | public static DstPlayerEqualityComparer Instance { get; } = new DstPlayerEqualityComparer();
9 |
10 | public bool Equals(DstPlayer? x, DstPlayer? y) => x?.Id == y?.Id;
11 |
12 | public int GetHashCode([DisallowNull] DstPlayer obj) => obj.Id.GetHashCode();
13 | }
14 |
--------------------------------------------------------------------------------
/DstServerQuery.Web/Helpers/Helper.cs:
--------------------------------------------------------------------------------
1 | using System.Buffers;
2 | using DstDownloaders;
3 | using DstServerQuery.Web.Models.Configurations;
4 | using SteamDownloader;
5 | using SteamDownloader.Helpers;
6 | using SteamKit2;
7 |
8 | namespace DstServerQuery.Web.Helpers;
9 |
10 | public static class Helper
11 | {
12 | public static string GetRandomColor(int minGray, int maxGray)
13 | {
14 | ArgumentOutOfRangeException.ThrowIfNegative(minGray);
15 | ArgumentOutOfRangeException.ThrowIfGreaterThan(maxGray, 255);
16 |
17 | var temp = ArrayPool.Shared.Rent(3);
18 | int gray;
19 |
20 | do
21 | {
22 | Random.Shared.NextBytes(temp.AsSpan()[0..3]);
23 | gray = (int)(0.299f * temp[0] + 0.587f * temp[1] + 0.114f * temp[2]);
24 | } while (gray < minGray || gray > maxGray);
25 |
26 | var colorHex = $"{temp[0]:X2}{temp[1]:X2}{temp[2]:X2}";
27 | ArrayPool.Shared.Return(temp);
28 |
29 | return colorHex;
30 | }
31 |
32 | public static SteamSession CreateSteamSession(IServiceProvider serviceProvider)
33 | {
34 | var steamOptions = serviceProvider.GetRequiredService();
35 | return new SteamSession(SteamConfiguration.Create(steamBuilder =>
36 | {
37 | if (steamOptions.SteampoweredApiProxy is { })
38 | {
39 | steamBuilder.WithWebAPIBaseAddress(steamOptions.SteampoweredApiProxy);
40 | }
41 | if (steamOptions.WebApiKey is { })
42 | {
43 | steamBuilder.WithWebAPIKey(steamOptions.WebApiKey);
44 | }
45 | }));
46 | }
47 |
48 | public static async Task EnsureContentServerAsync(DstDownloader dst, CancellationToken cancellationToken = default)
49 | {
50 | var servers1 = await dst.Steam.GetCdnServersAsync(1, null, cancellationToken);
51 | var servers2 = await dst.Steam.GetCdnServersAsync(100, null, cancellationToken);
52 | var servers3 = await dst.Steam.GetCdnServersAsync(150, null, cancellationToken);
53 | var servers4 = await dst.Steam.GetCdnServersAsync(200, null, cancellationToken);
54 | IEnumerable servers = [.. servers1, .. servers2, .. servers3, .. servers4];
55 | var stableServers = await SteamHelper.TestContentServerConnectionAsync(dst.Steam.HttpClient, servers, TimeSpan.FromSeconds(4));
56 | dst.Steam.ContentServers = stableServers.ToList();
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/DstServerQuery.Web/Helpers/ServerQueryer/JsonConverter/RegexValueJsonConverter.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json;
2 | using System.Text.Json.Serialization;
3 |
4 | namespace DstServerQuery.Web.Helpers.ServerQueryer.JsonConverter;
5 |
6 | public class RegexValueJsonConverter : JsonConverter
7 | {
8 | public override RegexValue Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
9 | {
10 | if (reader.TokenType == JsonTokenType.Null)
11 | {
12 | return new RegexValue();
13 | }
14 | if (reader.TokenType == JsonTokenType.String)
15 | {
16 | return new RegexValue()
17 | {
18 | Value = reader.GetString(),
19 | };
20 | }
21 | else
22 | {
23 | return JsonSerializer.Deserialize(ref reader);
24 | }
25 | }
26 |
27 | public override void Write(Utf8JsonWriter writer, RegexValue value, JsonSerializerOptions options)
28 | {
29 | JsonSerializer.Serialize(writer, value);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/DstServerQuery.Web/Helpers/ServerQueryer/JsonConverter/StringArrayJsonConverter.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json;
2 | using System.Text.Json.Serialization;
3 |
4 | namespace DstServerQuery.Web.Helpers.ServerQueryer.JsonConverter;
5 |
6 | public class StringArrayJsonConverter : JsonConverter
7 | {
8 | public override StringArray Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
9 | {
10 | if (reader.TokenType == JsonTokenType.Null)
11 | {
12 | return new StringArray();
13 | }
14 | else if (reader.TokenType is JsonTokenType.String or JsonTokenType.StartArray)
15 | {
16 | return new()
17 | {
18 | Value = StringSplitConverter.Instance.Read(ref reader, typeof(string[]), options)
19 | };
20 | }
21 | else if (reader.TokenType == JsonTokenType.Number)
22 | {
23 | return new()
24 | {
25 | Value = [reader.GetInt64().ToString()]
26 | };
27 | }
28 | else
29 | {
30 | return JsonSerializer.Deserialize(ref reader, new JsonSerializerOptions()
31 | {
32 |
33 | });
34 | }
35 | }
36 |
37 | public override void Write(Utf8JsonWriter writer, StringArray value, JsonSerializerOptions options)
38 | {
39 | JsonSerializer.Serialize(writer, value);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/DstServerQuery.Web/Helpers/ServerQueryer/JsonConverter/StringSplitConverter.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json;
2 | using System.Text.Json.Serialization;
3 |
4 | namespace DstServerQuery.Web.Helpers.ServerQueryer.JsonConverter;
5 |
6 | public class StringSplitConverter : JsonConverter
7 | {
8 | public static char[] SplitChars { get; } = [';', '|', ','];
9 |
10 | public static StringSplitConverter Instance { get; } = new();
11 |
12 | public override string?[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
13 | {
14 | if (reader.TokenType == JsonTokenType.String)
15 | {
16 | var str = reader.GetString()!;
17 | return str.Split(SplitChars, StringSplitOptions.RemoveEmptyEntries)
18 | .Where(v => !string.IsNullOrWhiteSpace(v))
19 | .ToArray();
20 | }
21 | else if (reader.TokenType == JsonTokenType.StartArray)
22 | {
23 | List arr = new();
24 | while (true)
25 | {
26 | if (reader.Read())
27 | {
28 | if (reader.TokenType == JsonTokenType.EndArray)
29 | break;
30 |
31 | if (reader.TokenType == JsonTokenType.String)
32 | {
33 | arr.Add(reader.GetString());
34 | }
35 | else if (reader.TokenType == JsonTokenType.Number)
36 | {
37 | arr.Add(reader.GetInt64().ToString());
38 | }
39 | }
40 | }
41 | return arr.ToArray();
42 | }
43 | else
44 | {
45 | return JsonSerializer.Deserialize(ref reader);
46 | }
47 | }
48 |
49 | public override void Write(Utf8JsonWriter writer, string?[] value, JsonSerializerOptions options)
50 | {
51 | JsonSerializer.Serialize(writer, value);
52 | }
53 | }
--------------------------------------------------------------------------------
/DstServerQuery.Web/Helpers/ServerQueryer/JsonConverter/ToStringJsonConverter.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json;
2 | using System.Text.Json.Serialization;
3 |
4 | namespace DstServerQuery.Web.Helpers.ServerQueryer.JsonConverter;
5 |
6 | public class ToStringJsonConverter : JsonConverter
7 | {
8 | public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
9 | {
10 | if (reader.TokenType == JsonTokenType.String)
11 | {
12 | return reader.GetString();
13 | }
14 | else if (reader.TokenType == JsonTokenType.Number)
15 | {
16 | return reader.GetInt64().ToString();
17 | }
18 | throw new JsonException();
19 | }
20 |
21 | public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)
22 | {
23 | writer.WriteStringValue(value);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/DstServerQuery.Web/Migrations/SqlServer/20231110030739_PlayerNotNull.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore.Migrations;
2 |
3 | #nullable disable
4 |
5 | namespace DstServerQuery.Web.Migrations.SqlServer
6 | {
7 | ///
8 | public partial class PlayerNotNull : Migration
9 | {
10 | ///
11 | protected override void Up(MigrationBuilder migrationBuilder)
12 | {
13 | migrationBuilder.AlterColumn(
14 | name: "Name",
15 | table: "Players",
16 | type: "nvarchar(max)",
17 | nullable: false,
18 | defaultValue: "",
19 | oldClrType: typeof(string),
20 | oldType: "nvarchar(max)",
21 | oldNullable: true);
22 | }
23 |
24 | ///
25 | protected override void Down(MigrationBuilder migrationBuilder)
26 | {
27 | migrationBuilder.AlterColumn(
28 | name: "Name",
29 | table: "Players",
30 | type: "nvarchar(max)",
31 | nullable: true,
32 | oldClrType: typeof(string),
33 | oldType: "nvarchar(max)");
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/DstServerQuery.Web/Migrations/SqlServer/20231207104123_AddTagColor.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore.Migrations;
2 |
3 | #nullable disable
4 |
5 | namespace DstServerQuery.Web.Migrations.SqlServer
6 | {
7 | ///
8 | public partial class AddTagColor : Migration
9 | {
10 | ///
11 | protected override void Up(MigrationBuilder migrationBuilder)
12 | {
13 | migrationBuilder.CreateTable(
14 | name: "TagColors",
15 | columns: table => new
16 | {
17 | Name = table.Column(type: "nvarchar(450)", nullable: false),
18 | TagColors = table.Column(type: "nvarchar(max)", nullable: false)
19 | },
20 | constraints: table =>
21 | {
22 | table.PrimaryKey("PK_TagColors", x => x.Name);
23 | });
24 |
25 | migrationBuilder.CreateIndex(
26 | name: "IX_TagColors_Name",
27 | table: "TagColors",
28 | column: "Name",
29 | unique: true);
30 | }
31 |
32 | ///
33 | protected override void Down(MigrationBuilder migrationBuilder)
34 | {
35 | migrationBuilder.DropTable(
36 | name: "TagColors");
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/DstServerQuery.Web/Migrations/SqlServer/20231208134329_AddDateIndex.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore.Migrations;
2 |
3 | #nullable disable
4 |
5 | namespace DstServerQuery.Web.Migrations.SqlServer
6 | {
7 | ///
8 | public partial class AddDateIndex : Migration
9 | {
10 | ///
11 | protected override void Up(MigrationBuilder migrationBuilder)
12 | {
13 | migrationBuilder.CreateIndex(
14 | name: "IX_ServerHistoryItems_Id_DateTime",
15 | table: "ServerHistoryItems",
16 | columns: new[] { "Id", "DateTime" });
17 |
18 | migrationBuilder.CreateIndex(
19 | name: "IX_ServerHistories_Id_UpdateTime",
20 | table: "ServerHistories",
21 | columns: new[] { "Id", "UpdateTime" });
22 | }
23 |
24 | ///
25 | protected override void Down(MigrationBuilder migrationBuilder)
26 | {
27 | migrationBuilder.DropIndex(
28 | name: "IX_ServerHistoryItems_Id_DateTime",
29 | table: "ServerHistoryItems");
30 |
31 | migrationBuilder.DropIndex(
32 | name: "IX_ServerHistories_Id_UpdateTime",
33 | table: "ServerHistories");
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/DstServerQuery.Web/Migrations/SqlServer/20231209045633_ToDateTimeOffset.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Microsoft.EntityFrameworkCore.Migrations;
3 |
4 | #nullable disable
5 |
6 | namespace DstServerQuery.Web.Migrations.SqlServer
7 | {
8 | ///
9 | public partial class ToDateTimeOffset : Migration
10 | {
11 | ///
12 | protected override void Up(MigrationBuilder migrationBuilder)
13 | {
14 | migrationBuilder.AlterColumn(
15 | name: "DateTime",
16 | table: "ServerHistoryItems",
17 | type: "datetimeoffset",
18 | nullable: false,
19 | oldClrType: typeof(DateTime),
20 | oldType: "datetime2");
21 |
22 | migrationBuilder.AlterColumn(
23 | name: "UpdateDate",
24 | table: "ServerHistoryCountInfos",
25 | type: "datetimeoffset",
26 | nullable: false,
27 | oldClrType: typeof(DateTime),
28 | oldType: "datetime2");
29 |
30 | migrationBuilder.AlterColumn(
31 | name: "UpdateTime",
32 | table: "ServerHistories",
33 | type: "datetimeoffset",
34 | nullable: false,
35 | oldClrType: typeof(DateTime),
36 | oldType: "datetime2");
37 | }
38 |
39 | ///
40 | protected override void Down(MigrationBuilder migrationBuilder)
41 | {
42 | migrationBuilder.AlterColumn(
43 | name: "DateTime",
44 | table: "ServerHistoryItems",
45 | type: "datetime2",
46 | nullable: false,
47 | oldClrType: typeof(DateTimeOffset),
48 | oldType: "datetimeoffset");
49 |
50 | migrationBuilder.AlterColumn(
51 | name: "UpdateDate",
52 | table: "ServerHistoryCountInfos",
53 | type: "datetime2",
54 | nullable: false,
55 | oldClrType: typeof(DateTimeOffset),
56 | oldType: "datetimeoffset");
57 |
58 | migrationBuilder.AlterColumn(
59 | name: "UpdateTime",
60 | table: "ServerHistories",
61 | type: "datetime2",
62 | nullable: false,
63 | oldClrType: typeof(DateTimeOffset),
64 | oldType: "datetimeoffset");
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/DstServerQuery.Web/Migrations/SqlServer/20231212090059_AddDstPlayerIndex.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore.Migrations;
2 |
3 | #nullable disable
4 |
5 | namespace DstServerQuery.Web.Migrations.SqlServer
6 | {
7 | ///
8 | public partial class AddDstPlayerIndex : Migration
9 | {
10 | ///
11 | protected override void Up(MigrationBuilder migrationBuilder)
12 | {
13 | migrationBuilder.DropIndex(
14 | name: "IX_ServerHistories_Id_UpdateTime",
15 | table: "ServerHistories");
16 |
17 | migrationBuilder.AlterColumn(
18 | name: "Name",
19 | table: "ServerHistories",
20 | type: "nvarchar(450)",
21 | nullable: false,
22 | oldClrType: typeof(string),
23 | oldType: "nvarchar(max)");
24 |
25 | migrationBuilder.AlterColumn(
26 | name: "Name",
27 | table: "Players",
28 | type: "nvarchar(450)",
29 | nullable: false,
30 | oldClrType: typeof(string),
31 | oldType: "nvarchar(max)");
32 |
33 | migrationBuilder.CreateIndex(
34 | name: "IX_ServerHistories_Id_UpdateTime_Name",
35 | table: "ServerHistories",
36 | columns: new[] { "Id", "UpdateTime", "Name" });
37 |
38 | migrationBuilder.CreateIndex(
39 | name: "IX_Players_Name_Platform",
40 | table: "Players",
41 | columns: new[] { "Name", "Platform" });
42 | }
43 |
44 | ///
45 | protected override void Down(MigrationBuilder migrationBuilder)
46 | {
47 | migrationBuilder.DropIndex(
48 | name: "IX_ServerHistories_Id_UpdateTime_Name",
49 | table: "ServerHistories");
50 |
51 | migrationBuilder.DropIndex(
52 | name: "IX_Players_Name_Platform",
53 | table: "Players");
54 |
55 | migrationBuilder.AlterColumn(
56 | name: "Name",
57 | table: "ServerHistories",
58 | type: "nvarchar(max)",
59 | nullable: false,
60 | oldClrType: typeof(string),
61 | oldType: "nvarchar(450)");
62 |
63 | migrationBuilder.AlterColumn(
64 | name: "Name",
65 | table: "Players",
66 | type: "nvarchar(max)",
67 | nullable: false,
68 | oldClrType: typeof(string),
69 | oldType: "nvarchar(450)");
70 |
71 | migrationBuilder.CreateIndex(
72 | name: "IX_ServerHistories_Id_UpdateTime",
73 | table: "ServerHistories",
74 | columns: new[] { "Id", "UpdateTime" });
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/DstServerQuery.Web/Models/Configurations/DstModsFileServiceOptions.cs:
--------------------------------------------------------------------------------
1 | namespace DstServerQuery.Web.Models.Configurations;
2 |
3 | public class DstModsFileServiceOptions
4 | {
5 | public bool IsEnabled { get; set; }
6 | public string RootPath { get; set; } = "mods";
7 | ///
8 | /// template: {url}
9 | ///
10 | public string? FileUrlProxy { get; set; }
11 | public bool IsEnableMultiLanguage { get; set; }
12 | }
13 |
--------------------------------------------------------------------------------
/DstServerQuery.Web/Models/Configurations/DstVersionServiceOptions.cs:
--------------------------------------------------------------------------------
1 | namespace DstServerQuery.Web.Models.Configurations;
2 |
3 | public class DstVersionServiceOptions
4 | {
5 | public bool IsEnabled { get; set; }
6 | public long? DefaultVersion { get; set; }
7 | public bool IsDisabledUpdate { get; set; }
8 | }
9 |
--------------------------------------------------------------------------------
/DstServerQuery.Web/Models/Configurations/SteamOptions.cs:
--------------------------------------------------------------------------------
1 | namespace DstServerQuery.Web.Models.Configurations;
2 |
3 | public class SteamOptions
4 | {
5 | public string? WebApiKey { get; set; }
6 | public Uri? SteampoweredApiProxy { get; set; }
7 | }
8 |
--------------------------------------------------------------------------------
/DstServerQuery.Web/Models/DstModCount.cs:
--------------------------------------------------------------------------------
1 | namespace DstServerQuery.Web.Models;
2 |
3 | public class DstModCount(long id, string name, int count)
4 | {
5 | public long Id { get; set; } = id;
6 | public string Name { get; set; } = name;
7 | public int Count { get; set; } = count;
8 | }
--------------------------------------------------------------------------------
/DstServerQuery.Web/Models/DstModNameCount.cs:
--------------------------------------------------------------------------------
1 | namespace DstServerQuery.Web.Models;
2 |
3 | public class DstModNameCount(string name, int count)
4 | {
5 | public string Name { get; set; } = name;
6 | public int Count { get; set; } = count;
7 | }
8 |
--------------------------------------------------------------------------------
/DstServerQuery.Web/Models/DstServerQueryWeb.cs:
--------------------------------------------------------------------------------
1 | namespace DstServerQuery.Web.Models;
2 |
3 | public class DstServerQueryWeb;
4 |
--------------------------------------------------------------------------------
/DstServerQuery.Web/Models/Http/ColorResponse.cs:
--------------------------------------------------------------------------------
1 | namespace DstServerQuery.Web.Models.Http;
2 |
3 | ///
4 | /// 颜色键值对响应
5 | ///
6 | public class ColorResponse : ResponseBase
7 | {
8 | ///
9 | /// Name:FFFFFF
10 | ///
11 | public required Dictionary Colors { get; set; }
12 | }
--------------------------------------------------------------------------------
/DstServerQuery.Web/Models/Http/GetPlayersResponse.cs:
--------------------------------------------------------------------------------
1 | using DstServerQuery.Web.Models;
2 |
3 | namespace DstServerQuery.Web.Models.Http;
4 |
5 | public class GetPlayersResponse : ResponseBase
6 | {
7 | ///
8 | /// 玩家列表
9 | ///
10 | public required ICollection List { get; set; }
11 |
12 | ///
13 | /// 所有个数
14 | ///
15 | public required int TotalCount { get; set; }
16 |
17 | ///
18 | /// 当前页个数
19 | ///
20 | public int Count => List.Count;
21 |
22 | ///
23 | /// 页索引
24 | ///
25 | public required int PageIndex { get; set; }
26 |
27 | ///
28 | /// 最大页索引
29 | ///
30 | public required int MaxPageIndex { get; set; }
31 | }
32 |
--------------------------------------------------------------------------------
/DstServerQuery.Web/Models/Http/GetServerVersionResponse.cs:
--------------------------------------------------------------------------------
1 | namespace DstServerQuery.Web.Models.Http;
2 |
3 | public class GetServerVersionResponse : ResponseBase
4 | {
5 | ///
6 | /// 服务器版本
7 | ///
8 | public long? Version { get; set; }
9 |
10 | public GetServerVersionResponse(long? version)
11 | {
12 | Version = version;
13 | if (version is null)
14 | {
15 | Code = 503;
16 | Error = "Service Unavailable";
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/DstServerQuery.Web/Models/Http/GetTotalResponse.cs:
--------------------------------------------------------------------------------
1 | namespace DstServerQuery.Web.Models.Http;
2 |
3 | public class GetTotalResponse : ResponseBase
4 | {
5 | public long? Version { get; set; }
6 | public int Connections { get; set; }
7 | public int Servers { get; set; }
8 |
9 | public DateTimeOffset DateTime { get; set; }
10 | }
11 |
--------------------------------------------------------------------------------
/DstServerQuery.Web/Models/Http/ListResponse.cs:
--------------------------------------------------------------------------------
1 | using DstServerQuery.Models.Lobby.Interfaces.V2;
2 |
3 | namespace DstServerQuery.Web.Models.Http;
4 |
5 | ///
6 | /// List响应结果
7 | ///
8 | ///
9 | public class ListResponse : ResponseBase where T : ILobbyServerV2
10 | {
11 | ///
12 | /// Http开始响应的时间
13 | ///
14 | public DateTimeOffset DateTime { get; set; }
15 |
16 | ///
17 | /// 数据最后更新时间
18 | ///
19 | public DateTimeOffset LastUpdate { get; set; }
20 |
21 | ///
22 | /// 当前页个数
23 | ///
24 | public int Count { get; set; }
25 |
26 | ///
27 | /// 所有个数
28 | ///
29 | public int TotalCount { get; set; }
30 |
31 | ///
32 | /// 当前页索引
33 | ///
34 | public int PageIndex { get; set; }
35 |
36 | ///
37 | /// 最大页索引
38 | ///
39 | public int MaxPageIndex { get; set; }
40 |
41 | ///
42 | /// 服务器列表
43 | ///
44 | public IEnumerable List { get; set; } = Array.Empty();
45 | }
46 |
47 |
--------------------------------------------------------------------------------
/DstServerQuery.Web/Models/Http/Mods/DstModsInfoResponse.cs:
--------------------------------------------------------------------------------
1 | using DstDownloaders.Mods;
2 | using SteamDownloader.WebApi.Interfaces;
3 |
4 | namespace DstServerQuery.Web.Models.Http.Mods;
5 |
6 |
7 | public class DstModsInfoRequest
8 | {
9 | public ulong WorkshopId { get; set; }
10 | public PublishedFileServiceLanguage? Language { get; set; }
11 | }
12 |
13 |
14 |
15 | public class DstModsInfoResponse : ResponseBase
16 | {
17 | ///
18 | /// Mod信息
19 | ///
20 | public required WebModsInfo Mod { get; set; }
21 | }
22 |
23 |
24 | ///
25 | /// Mod信息
26 | ///
27 | public class WebModsInfo : WebModsInfoLite
28 | {
29 | ///
30 | /// Mod占用大小
31 | ///
32 | public long Size { get; set; }
33 |
34 | ///
35 | /// 浏览信息
36 | ///
37 | public SteamModsView View { get; set; }
38 |
39 | ///
40 | /// 配置选项
41 | ///
42 | public DstConfigurationOption[]? ConfigurationOptions { get; set; }
43 |
44 | public WorkshopPreview[]? Previews { get; set; }
45 |
46 | public SteamVoteData? VoteData { get; set; }
47 |
48 | public WebModsInfo(DstModStore store, PublishedFileServiceLanguage? language) : base(store, language)
49 | {
50 | AuthorSteamId = store.SteamModInfo!.CreatorSteamId;
51 | IsUGC = store.SteamModInfo.IsUGC;
52 | Size = store.ExtInfo.Size;
53 | View = new((int)store.SteamModInfo.Views,
54 | (int)store.SteamModInfo.Subscriptions,
55 | (int)store.SteamModInfo.Favorited,
56 | store.SteamModInfo.CommentsPublic);
57 |
58 | ConfigurationOptions = store.ModInfoLua?.ConfigurationOptions;
59 |
60 | if (store.SteamModInfo.details.VoteData is { } voteData)
61 | VoteData = new(voteData.Score, voteData.VotesUp, voteData.VotesDown);
62 | if (store.SteamModInfo.details.Previews is { } previews)
63 | Previews = previews.Select(v => new WorkshopPreview(v.PrewviewId, v.SortOrder, v.Url, v.Size, v.FileName, v.PreviewType)).ToArray();
64 | }
65 | }
66 |
67 | ///
68 | ///
69 | ///
70 | /// 不重复访客数
71 | /// 当前订阅者
72 | /// 当前收藏人数
73 | /// 公开评论数量
74 | public record SteamModsView(int Views, int Subscriptions, int Favorited, int CommentsPublic);
75 |
76 | public record WorkshopPreview(ulong PrewviewId, uint SortOrder, Uri? Url, long Size, string? FileName, uint PreviewType);
77 |
78 | public record SteamVoteData(double Score, int VotesUp, int VotesDown);
79 |
--------------------------------------------------------------------------------
/DstServerQuery.Web/Models/Http/Mods/DstModsNameUsageResponse.cs:
--------------------------------------------------------------------------------
1 | namespace DstServerQuery.Web.Models.Http.Mods;
2 |
3 | public class DstModsNameUsageResponse : ResponseBase
4 | {
5 | public IEnumerable Mods { get; set; }
6 |
7 | }
8 |
--------------------------------------------------------------------------------
/DstServerQuery.Web/Models/Http/Mods/DstModsUsageResponse.cs:
--------------------------------------------------------------------------------
1 | namespace DstServerQuery.Web.Models.Http.Mods;
2 |
3 | public class DstModsUsageResponse : ResponseBase
4 | {
5 | public IEnumerable Mods { get; set; }
6 | }
--------------------------------------------------------------------------------
/DstServerQuery.Web/Models/Http/Mods/ModsQueryResponse.cs:
--------------------------------------------------------------------------------
1 | using DstDownloaders.Mods;
2 | using SteamDownloader.WebApi.Interfaces;
3 |
4 | namespace DstServerQuery.Web.Models.Http.Mods;
5 |
6 | ///
7 | /// Mods查询参数
8 | ///
9 | public record QueryModsParams
10 | {
11 | public string? Text { get; set; } = string.Empty;
12 |
13 | public int? PageIndex { get; set; } = 0;
14 |
15 | public int? PageSize { get; set; } = 100;
16 |
17 | public bool? IsQueryName { get; set; } = true;
18 | public bool? IsQueryDescription { get; set; } = true;
19 | public bool? IsQueryAuthor { get; set; } = true;
20 | public bool? IsQueryTag { get; set; } = true;
21 | public bool? IsQueryWorkshopId { get; set; } = true;
22 |
23 | public bool? IgnoreCase { get; set; } = true;
24 |
25 | ///
26 | /// 排序, +表示升序, -表示降序, 默认升序
27 | ///
28 | ///
29 | public string? Sort { get; set; }
30 |
31 | public DstModType? Type { get; set; }
32 |
33 | public PublishedFileServiceLanguage? Language { get; set; }
34 |
35 | public enum SortType
36 | {
37 | Relevance = default,
38 | UpdateTime,
39 | CreatedTime,
40 | Name,
41 | WorkshopId,
42 | Size,
43 | Views,
44 | Subscriptions,
45 | Favorited,
46 | CommentsPublic,
47 | }
48 | }
49 |
50 |
51 |
52 | public class WebModsInfoLite
53 | {
54 | ///
55 | /// 创意工坊Id
56 | ///
57 | public ulong WorkshopId { get; set; }
58 |
59 | ///
60 | /// 名称
61 | ///
62 | public string Name { get; set; }
63 |
64 | ///
65 | /// 描述
66 | ///
67 | public string? Description { get; set; }
68 |
69 |
70 | ///
71 | /// Mod的更新时间
72 | ///
73 | public DateTimeOffset UpdateTime { get; set; }
74 |
75 | ///
76 | /// Mod的创建时间
77 | ///
78 | public DateTimeOffset CreatedTime { get; set; }
79 |
80 |
81 | ///
82 | /// 创建者
83 | ///
84 | public string? Author { get; set; }
85 |
86 | ///
87 | /// 创建者的SteamId
88 | ///
89 | public ulong AuthorSteamId { get; set; }
90 |
91 | ///
92 | /// 标签
93 | ///
94 | public string[] Tags { get; set; }
95 |
96 | ///
97 | /// 服务端Mod还是客户端Mod
98 | ///
99 | public DstModType? ModType { get; set; }
100 |
101 | ///
102 | /// 是否是 UGC Mod
103 | ///
104 | public bool IsUGC { get; set; }
105 |
106 | ///
107 | /// 最新版本号
108 | ///
109 | public string? Version { get; set; }
110 |
111 | ///
112 | /// 预览图片链接
113 | ///
114 | public Uri? PreviewImageUrl { get; set; }
115 | public string? PreviewImageType { get; set; }
116 |
117 | //public bool IsDescriptionMarkup { get; set; }
118 |
119 | public PublishedFileServiceLanguage Language { get; set; }
120 |
121 | public WebModsInfoLite(DstModStore store, PublishedFileServiceLanguage? language = null)
122 | {
123 | if (store is null)
124 | {
125 | throw new NullReferenceException("store为null");
126 | }
127 | if (store.SteamModInfo is null)
128 | {
129 | throw new NullReferenceException($"SteamModInfo为null WorkshopId:{store.WorkshopId}");
130 | }
131 |
132 | WorkshopId = store.WorkshopId;
133 | Name = store.SteamModInfo!.Name!;
134 | Description = store.SteamModInfo.Description;
135 |
136 | //获取指定语言的描述
137 | Language = PublishedFileServiceLanguage.English;
138 | if (language != null && store.ExtInfo.MultiLanguage?.TryGetValue(language.Value, out var data) is true)
139 | {
140 | Name = data.Name ?? Name;
141 | Description = data.Description;
142 | Language = language.Value;
143 | }
144 |
145 | UpdateTime = store.UpdatedTime!.Value;
146 | CreatedTime = store.SteamModInfo.CreatedTime;
147 |
148 | Author = store.ModInfoLua?.Author;
149 | ModType = store.ModInfoLua?.DstModType;
150 | Version = store.ModInfoLua?.Version;
151 | Tags = store.SteamModInfo.Tags ?? [];
152 |
153 | PreviewImageUrl = store.SteamModInfo.PreviewImageUrl;
154 | PreviewImageType = store.ExtInfo.PreviewImageType;
155 | }
156 | }
157 |
158 | public class ModsQueryResponse : ResponseBase
159 | {
160 | public IReadOnlyCollection? Mods { get; set; }
161 | public int PageIndex { get; set; }
162 | public int Count { get; set; }
163 | public int MaxPageIndex { get; set; }
164 | public int TotalCount { get; set; }
165 | }
166 |
--------------------------------------------------------------------------------
/DstServerQuery.Web/Models/Http/PlayerServerHistoryResponse.cs:
--------------------------------------------------------------------------------
1 | namespace DstServerQuery.Web.Models.Http;
2 |
3 | public class PlayerServerHistoryResponse : ResponseBase
4 | {
5 | public string[] Servers { get; set; } = [];
6 | }
7 |
--------------------------------------------------------------------------------
/DstServerQuery.Web/Models/Http/PrefabsResponse.cs:
--------------------------------------------------------------------------------
1 | namespace DstServerQuery.Web.Models.Http;
2 |
3 | public class PrefabsResponse(IEnumerable prefabs) : ResponseBase
4 | {
5 | public IEnumerable Prefabs { get; } = prefabs;
6 |
7 | public record PlayerPrefab(string Prefab, int Count);
8 | }
9 |
--------------------------------------------------------------------------------
/DstServerQuery.Web/Models/Http/ResponseBase.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc;
2 | using System.Text.Json.Serialization;
3 |
4 | namespace DstServerQuery.Web.Models.Http;
5 |
6 | ///
7 | /// 响应
8 | ///
9 | public class ResponseBase
10 | {
11 | ///
12 | /// 响应码
13 | ///
14 | public int Code { get; set; } = 200;
15 |
16 | ///
17 | /// 当Code不等于200时的错误消息
18 | ///
19 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
20 | public string? Error { get; set; }
21 |
22 | public static JsonResult From(ResponseBase model, object? serializerSettings = null)
23 | {
24 | return new JsonResult(model, serializerSettings) { StatusCode = model.Code };
25 | }
26 |
27 | public static JsonResult From(ResponseBase model, int statusCode, object? serializerSettings = null)
28 | {
29 | model.Code = statusCode;
30 | return new JsonResult(model, serializerSettings) { StatusCode = statusCode };
31 | }
32 |
33 | public static JsonResult NotFound(string errorMessage = "Not Found")
34 | {
35 | return new JsonResult(new ResponseBase() { Code = 404, Error = errorMessage }) { StatusCode = 404 };
36 | }
37 |
38 | public static JsonResult BadRequest(string errorMessage = "Bad Request")
39 | {
40 | return new JsonResult(new ResponseBase() { Code = 400, Error = errorMessage }) { StatusCode = 400 };
41 | }
42 |
43 | public JsonResult ToJsonResult(object? serializerSettings = null)
44 | {
45 | return new JsonResult(this, serializerSettings) { StatusCode = Code };
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/DstServerQuery.Web/Models/Http/ServerCountHistoryResponse.cs:
--------------------------------------------------------------------------------
1 | using DstServerQuery.EntityFrameworkCore.Model.Entities;
2 |
3 | namespace DstServerQuery.Web.Models.Http;
4 |
5 | public class ServerCountHistoryResponse : ResponseBase
6 | {
7 | public required ICollection List { get; set; }
8 | }
9 |
--------------------------------------------------------------------------------
/DstServerQuery.Web/Models/Http/ServerDetailsResponse.cs:
--------------------------------------------------------------------------------
1 | using DstServerQuery.Models.Lobby.Interfaces.V2;
2 |
3 | namespace DstServerQuery.Web.Models.Http;
4 |
5 | ///
6 | /// 服务器的详细信息
7 | ///
8 | public class ServerDetailsResponse : ResponseBase
9 | {
10 | public required ILobbyServerDetailedV2 Server { get; set; }
11 | public required DateTimeOffset LastUpdate { get; set; }
12 | }
13 |
--------------------------------------------------------------------------------
/DstServerQuery.Web/Models/Http/ServerHistoryResponse.cs:
--------------------------------------------------------------------------------
1 | using DstServerQuery.EntityFrameworkCore.Model.Entities;
2 |
3 | namespace DstServerQuery.Web.Models.Http;
4 |
5 | public class ServerHistoryResponse : ResponseBase
6 | {
7 | public required DstServerHistory Server { get; set; }
8 | public required IEnumerable Items { get; set; }
9 | }
10 |
--------------------------------------------------------------------------------
/DstServerQuery.Web/Models/Http/TagsResponse.cs:
--------------------------------------------------------------------------------
1 | namespace DstServerQuery.Web.Models.Http;
2 |
3 | public class TagsResponse(IEnumerable tags) : ResponseBase
4 | {
5 | public IEnumerable Tags { get; } = tags;
6 |
7 | public record ServerTag(string Tag, int Count);
8 | }
9 |
--------------------------------------------------------------------------------
/DstServerQuery.Web/Models/PlayerInfoItem.cs:
--------------------------------------------------------------------------------
1 | using DstServerQuery.EntityFrameworkCore.Model.Entities;
2 | using DstServerQuery.Models;
3 |
4 | namespace DstServerQuery.Web.Models;
5 |
6 | ///
7 | /// 玩家信息
8 | ///
9 | public class PlayerInfoItem
10 | {
11 | ///
12 | /// 玩家Id
13 | ///
14 | public required string NetId { get; set; }
15 |
16 | ///
17 | /// 玩家名
18 | ///
19 | public required string Name { get; set; }
20 |
21 | ///
22 | /// 所在的平台
23 | ///
24 | public required Platform Platform { get; set; }
25 |
26 | public static PlayerInfoItem From(DstPlayer player)
27 | {
28 | return new()
29 | {
30 | NetId = player.Id,
31 | Name = player.Name,
32 | Platform = player.Platform,
33 | };
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/DstServerQuery.Web/Models/ServerHistoryItem.cs:
--------------------------------------------------------------------------------
1 | using DstServerQuery.EntityFrameworkCore.Model.Entities;
2 | using System.Text.Json.Serialization;
3 |
4 | namespace DstServerQuery.Web.Models;
5 |
6 | public record ServerHistoryItem
7 | {
8 | public string? Season { get; set; }
9 | public int PlayerCount { get; set; }
10 | public DateTimeOffset DateTime { get; set; }
11 |
12 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
13 | public DstDaysInfo? DaysInfo { get; set; }
14 |
15 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
16 | public ICollection? Players { get; set; }
17 |
18 | public static ServerHistoryItem From(DstServerHistoryItem item)
19 | {
20 | return new()
21 | {
22 | Season = item.Season,
23 | DateTime = item.DateTime,
24 | PlayerCount = item.PlayerCount,
25 | DaysInfo = item.IsDetailed ? item.DaysInfo : null,
26 | Players = item.IsDetailed ? item.Players : null,
27 | };
28 | }
29 | }
--------------------------------------------------------------------------------
/DstServerQuery.Web/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "profiles": {
3 | "Web": {
4 | "commandName": "Project",
5 | "launchBrowser": true,
6 | "launchUrl": "swagger",
7 | "environmentVariables": {
8 | "ASPNETCORE_ENVIRONMENT": "Development"
9 | },
10 | "dotnetRunMessages": true,
11 | "applicationUrl": "http://localhost:3000"
12 | },
13 | "Web NoBrowser": {
14 | "commandName": "Project",
15 | "launchUrl": "swagger",
16 | "environmentVariables": {
17 | "ASPNETCORE_ENVIRONMENT": "Development"
18 | },
19 | "dotnetRunMessages": true,
20 | "applicationUrl": "http://localhost:3000"
21 | },
22 | "Https": {
23 | "commandName": "Project",
24 | "dotnetRunMessages": true,
25 | "launchBrowser": true,
26 | "launchUrl": "swagger",
27 | "applicationUrl": "https://localhost:3001",
28 | "environmentVariables": {
29 | "ASPNETCORE_ENVIRONMENT": "Development"
30 | }
31 | },
32 | "WSL": {
33 | "commandName": "WSL2",
34 | "launchBrowser": true,
35 | "launchUrl": "http://localhost:2400/swagger",
36 | "environmentVariables": {
37 | "ASPNETCORE_ENVIRONMENT": "Development",
38 | "ASPNETCORE_URLS": "http://localhost:2400"
39 | },
40 | "distributionName": ""
41 | }
42 | },
43 | "$schema": "https://json.schemastore.org/launchsettings.json"
44 | }
--------------------------------------------------------------------------------
/DstServerQuery.Web/Services/AppHostedService.cs:
--------------------------------------------------------------------------------
1 | using DstDownloaders;
2 | using DstDownloaders.Mods;
3 | using DstServerQuery.EntityFrameworkCore;
4 | using DstServerQuery.EntityFrameworkCore.Model;
5 | using DstServerQuery.Helpers;
6 | using DstServerQuery.Models.Requests;
7 | using DstServerQuery.Services;
8 | using DstServerQuery.Web.Helpers;
9 | using DstServerQuery.Web.Models.Configurations;
10 | using Microsoft.EntityFrameworkCore;
11 |
12 | namespace DstServerQuery.Web.Services;
13 |
14 | public class AppHostedService(ILogger _logger,
15 | IServiceProvider _serviceProvider,
16 | IConfiguration _configuration,
17 | DstWebConfig _dstWebConfig,
18 | SteamOptions _steamOptions,
19 | DstVersionService _dstVersionService,
20 | DstVersionServiceOptions _dstVersionServiceOptions,
21 | DstModsFileServiceOptions _dstModsFileServiceOptions,
22 | HistoryCountService _historyCountService,
23 | LobbyServerManager _lobbyServerManager,
24 | GeoIPService _geoIPService) : IHostedService
25 | {
26 | private DstModsFileService? _dstModsFileService;
27 |
28 | public async Task StartAsync(CancellationToken cancellationToken)
29 | {
30 | _dstModsFileService = _serviceProvider.GetService();
31 |
32 | using var scope = _serviceProvider.CreateScope();
33 | _logger.LogInformation("IHostApplicationBuilder Start");
34 |
35 | SimpleCacheDatabase simpleCacheDatabase = scope.ServiceProvider.GetRequiredService();
36 |
37 | // 键值对缓存数据库
38 | simpleCacheDatabase.EnsureInitialize();
39 |
40 | // 数据库迁移
41 | using var dbContext = scope.ServiceProvider.GetRequiredService();
42 | bool isMigration = false;
43 | try
44 | {
45 | isMigration = dbContext.Database.GetPendingMigrations().Any();
46 | }
47 | catch { }
48 | if (isMigration)
49 | {
50 | dbContext.Database.SetCommandTimeout(TimeSpan.FromMinutes(100));
51 | await dbContext.Database.MigrateAsync(cancellationToken); //执行迁移
52 | _logger.LogInformation("数据库迁移成功");
53 | }
54 | else
55 | {
56 | await dbContext.Database.EnsureCreatedAsync(cancellationToken);
57 | _logger.LogInformation("数据库创建成功");
58 | }
59 |
60 | try
61 | {
62 | await _historyCountService.Initialize();
63 | }
64 | catch (Exception ex)
65 | {
66 | _logger.LogError(ex, "HistoryCountManager初始化失败");
67 | }
68 |
69 |
70 | // 配置GeoIP
71 | if (_configuration.GetValue("GeoLite2Path") is string geoLite2Path)
72 | {
73 | try
74 | {
75 | _geoIPService.Initialize(geoLite2Path);
76 | }
77 | catch (Exception ex)
78 | {
79 | _logger?.LogError(ex, "GeoIP初始化异常");
80 | return;
81 | }
82 | DstConverterHelper.GeoIPService = _geoIPService;
83 | }
84 |
85 | // 启动服务管理器
86 | await _lobbyServerManager.Start();
87 |
88 | // 饥荒版本获取服务
89 | _dstVersionService.DstDownloaderFactory = () =>
90 | {
91 | return new DstDownloader(Helper.CreateSteamSession(_serviceProvider));
92 | };
93 | var defaultVersion = simpleCacheDatabase.Get("DstVersion") ?? _dstVersionServiceOptions.DefaultVersion;
94 | _ = _dstVersionService.RunAsync(defaultVersion, _dstVersionServiceOptions.IsDisabledUpdate);
95 | _dstVersionService.VersionUpdated += (sender, version) =>
96 | {
97 | using var scope = _serviceProvider.CreateScope();
98 | using var simpleCacheDatabase = scope.ServiceProvider.GetRequiredService();
99 | simpleCacheDatabase["DstVersion"] = version;
100 | };
101 |
102 | }
103 |
104 | public async Task StopAsync(CancellationToken cancellationToken)
105 | {
106 | _logger.LogInformation("IHostApplicationBuilder Shutdowning");
107 |
108 | _dstVersionService.Dispose();
109 | _lobbyServerManager.Dispose();
110 | _dstModsFileService?.Dispose();
111 |
112 | Serilog.Log.CloseAndFlush();
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/DstServerQuery.Web/Services/CommandService.cs:
--------------------------------------------------------------------------------
1 | using DstServerQuery.Web.Helpers.Commands;
2 | using DstServerQuery.Web.Helpers.Console;
3 | using PrettyPrompt;
4 | using PrettyPrompt.Highlighting;
5 | using System.CommandLine.Parsing;
6 |
7 | namespace DstServerQuery.Web.Services;
8 |
9 | public class CommandService(
10 | IHost host,
11 | ControllableConsoleSink controllableConsoleSink
12 | )
13 | {
14 | private readonly CancellationTokenSource cts = new();
15 | public Prompt? Prompt { get; private set; }
16 | public AppcalitionCommand Command { get; private set; } = new(host.Services);
17 | public CommandPromptCallbacks? CommandPromptCallbacks { get; set; }
18 |
19 | public async Task RunCommandLoopAsync()
20 | {
21 | Command.BuildParser();
22 | CommandPromptCallbacks = new(Command, controllableConsoleSink);
23 |
24 | Prompt = new(Path.Join(AppContext.BaseDirectory, "history_command.txt"), CommandPromptCallbacks, null, new PromptConfiguration(
25 | completionBoxBorderFormat: new ConsoleFormat(AnsiColor.Rgb(0x87, 0x6C, 0xB3)),
26 | selectedCompletionItemBackground: AnsiColor.Rgb(0x30, 0x30, 0x30)
27 | )
28 | {
29 | Prompt = new FormattedString(">>> ", new FormatSpan(0, 3, AnsiColor.BrightCyan)),
30 | });
31 |
32 | while (true)
33 | {
34 | var result = await Prompt!.ReadLineAsync();
35 |
36 | await Command.Parser!.InvokeAsync(result.Text);
37 | if (Command.IsExit || cts.Token.IsCancellationRequested)
38 | {
39 | controllableConsoleSink.Enabled = true;
40 | await host.StopAsync();
41 | break;
42 | }
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/DstServerQuery.Web/Services/HistoryCleanupService.cs:
--------------------------------------------------------------------------------
1 | using DstServerQuery.EntityFrameworkCore.Model;
2 | using DstServerQuery.EntityFrameworkCore.Model.Entities;
3 | using Microsoft.EntityFrameworkCore;
4 |
5 | namespace DstServerQuery.Web.Services;
6 |
7 | public class HistoryCleanupService(IServiceProvider serviceProvider, TimeSpan? expiration) : IHostedService
8 | {
9 | private readonly CancellationTokenSource cts = new();
10 |
11 | public async Task StartAsync(CancellationToken cancellationToken)
12 | {
13 | if (expiration == null)
14 | {
15 | return;
16 | }
17 | if (expiration.Value < default(TimeSpan))
18 | {
19 | return;
20 | }
21 |
22 | System.Timers.Timer timer = new(TimeSpan.FromHours(1));
23 | timer.Elapsed += Timer_Elapsed;
24 | timer.Start();
25 | }
26 |
27 | private async void Timer_Elapsed(object? sender, System.Timers.ElapsedEventArgs _)
28 | {
29 | using var scope = serviceProvider.CreateScope();
30 | using var db = scope.ServiceProvider.GetRequiredService();
31 | var logger = scope.ServiceProvider.GetRequiredService>();
32 |
33 | db.Database.SetCommandTimeout(TimeSpan.FromMinutes(10));
34 |
35 | try
36 | {
37 | var e = DateTimeOffset.UtcNow - expiration!.Value;
38 |
39 | while (true)
40 | {
41 | var historyServerItem = await db.ServerHistoryItems
42 | .Where(v => v.DateTime < e)
43 | .OrderBy(v => v.Id)
44 | .Take(10000)
45 | .Select(v => new DstServerHistoryItem()
46 | {
47 | Id = v.Id,
48 | DaysInfoId = v.DaysInfoId
49 | })
50 | .ToArrayAsync(cts.Token);
51 |
52 | if (historyServerItem is null or [])
53 | break;
54 |
55 | await db.HistoryServerItemPlayerPair
56 | .Where(v => historyServerItem.Select(v => v.Id).Contains(v.HistoryServerItemId))
57 | .ExecuteDeleteAsync(cts.Token);
58 |
59 | db.RemoveRange(historyServerItem);
60 |
61 | await db.SaveChangesAsync(cts.Token);
62 | }
63 | }
64 | catch (Exception e)
65 | {
66 | logger.LogError("History清理错误 {Exception}", e.Message);
67 | }
68 |
69 | try
70 | {
71 | await Task.Delay(TimeSpan.FromHours(1), cts.Token);
72 | }
73 | catch (TaskCanceledException)
74 | {
75 | return;
76 | }
77 | }
78 |
79 | public Task StopAsync(CancellationToken cancellationToken)
80 | {
81 | cts.Cancel();
82 | return Task.CompletedTask;
83 | }
84 | }
85 |
86 | public static class HistoryCleanupServiceExtensions
87 | {
88 | public static IServiceCollection AddHistoryCleanupService(this IServiceCollection serviceDescriptors, int? expirationHours)
89 | {
90 | if (expirationHours is null or < 0) return serviceDescriptors;
91 | serviceDescriptors.AddHostedService(v => new HistoryCleanupService(v, TimeSpan.FromHours(expirationHours.Value)));
92 | return serviceDescriptors;
93 | }
94 | }
--------------------------------------------------------------------------------
/DstServerQuery.Web/Services/TrafficRateLimiter/TrafficChunk.cs:
--------------------------------------------------------------------------------
1 | namespace DstServerQuery.Web.Services.TrafficRateLimiter;
2 |
3 | public record struct TrafficChunk
4 | {
5 | public DateTimeOffset DateTime { get; set; }
6 | public int Bytes { get; set; }
7 | }
8 |
--------------------------------------------------------------------------------
/DstServerQuery.Web/Services/TrafficRateLimiter/TrafficContext.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Concurrent;
2 |
3 | namespace DstServerQuery.Web.Services.TrafficRateLimiter;
4 |
5 | public record TrafficContext
6 | {
7 | public required string IP { get; set; }
8 | public required ConcurrentQueue Chunks { get; init; }
9 | }
--------------------------------------------------------------------------------
/DstServerQuery.Web/Services/TrafficRateLimiter/TrafficLimiterExtensions.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Http.Features;
2 | using System.Collections.Concurrent;
3 |
4 | namespace DstServerQuery.Web.Services.TrafficRateLimiter;
5 |
6 | public static class TrafficLimiterExtensions
7 | {
8 | public static IServiceCollection AddTrafficLimiter(this IServiceCollection serviceDescriptors, Action? configureOptions = null)
9 | {
10 | TrafficRateLimitOptions trafficRateLimitOptions = new();
11 | serviceDescriptors.AddSingleton(trafficRateLimitOptions);
12 | configureOptions?.Invoke(trafficRateLimitOptions);
13 |
14 | return serviceDescriptors;
15 | }
16 |
17 |
18 |
19 | public static IApplicationBuilder UseTrafficLimiter(this IApplicationBuilder applicationBuilder)
20 | {
21 | TrafficRateLimitOptions trafficRateLimitOptions = applicationBuilder.ApplicationServices.GetRequiredService();
22 |
23 | applicationBuilder.UseTrafficLimiter((context, trafficContext, next) =>
24 | {
25 | return Task.CompletedTask;
26 | });
27 |
28 | return applicationBuilder;
29 | }
30 |
31 | public static IApplicationBuilder UseTrafficLimiter(this IApplicationBuilder applicationBuilder, Func limitedCallback)
32 | {
33 | TrafficRateLimitOptions trafficRateLimitOptions = applicationBuilder.ApplicationServices.GetRequiredService();
34 |
35 | applicationBuilder.Use(async (httpContext, next) =>
36 | {
37 | IHttpResponseBodyFeature? originStream = httpContext.Features.Get()!;
38 | TrafficMonitorStream trafficMonitorStream = new(originStream);
39 | httpContext.Features.Set(trafficMonitorStream);
40 |
41 | string? ip = null;
42 | foreach (var key in trafficRateLimitOptions.IPHeader)
43 | {
44 | if (httpContext.Request.Headers.TryGetValue(key, out var value))
45 | {
46 | ip = value;
47 | break;
48 | }
49 | }
50 | ip ??= httpContext.Connection.RemoteIpAddress?.ToString() ?? "";
51 |
52 | TrafficRateLimit[]? limits = null;
53 | if (trafficRateLimitOptions.TrafficTargets?.TryGetValue(ip, out limits) != true)
54 | {
55 | limits = trafficRateLimitOptions.TrafficAny;
56 | }
57 | limits ??= [];
58 |
59 | if (!trafficRateLimitOptions.Users.TryGetValue(ip, out var chunks))
60 | {
61 | chunks = new ConcurrentQueue();
62 | trafficRateLimitOptions.Users[ip] = chunks;
63 | }
64 |
65 | foreach (var limit in limits)
66 | {
67 | DateTimeOffset start = DateTimeOffset.Now - TimeSpan.FromSeconds(limit.WindowSec);
68 | var range = chunks.Where(v => v.DateTime >= start);
69 | var sum = range.Sum(v => v.Bytes);
70 | TrafficContext trafficContext = new()
71 | {
72 | Chunks = chunks,
73 | IP = ip
74 | };
75 |
76 | if (sum > limit.TrafficBytes)
77 | {
78 | //请求被限制
79 | httpContext.Response.StatusCode = trafficRateLimitOptions.StatusCode;
80 | httpContext.Response.Headers.RetryAfter = limit.WindowSec.ToString();
81 | await limitedCallback(httpContext, trafficContext, next);
82 | httpContext.Features.Set(originStream);
83 | return;
84 | }
85 | }
86 |
87 | await next(httpContext);
88 | await trafficMonitorStream.FlushAsync();
89 |
90 | TrafficChunk currentChunk = new()
91 | {
92 | Bytes = trafficMonitorStream.Bytes,
93 | DateTime = DateTimeOffset.Now,
94 | };
95 | chunks.Enqueue(currentChunk);
96 |
97 | while (chunks.Count > trafficRateLimitOptions.MaxQueue)
98 | {
99 | chunks.TryDequeue(out var chunk);
100 | }
101 |
102 | //sum += currentChunk.Bytes;
103 | //bool isLimit = false;
104 | //if (sum > limit.TrafficBytes)
105 | //{
106 | // await limitedCallback(httpContext, trafficContext, next);
107 | //}
108 |
109 | //TrafficContext trafficContext = new()
110 | //{
111 | // Chunks = chunks,
112 | // Current = currentChunk,
113 | // IsLimit = isLimit,
114 | //};
115 |
116 | httpContext.Features.Set(originStream);
117 | });
118 |
119 | return applicationBuilder;
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/DstServerQuery.Web/Services/TrafficRateLimiter/TrafficMonitorStream.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Http.Features;
2 | using System.IO.Pipelines;
3 |
4 | namespace DstServerQuery.Web.Services.TrafficRateLimiter;
5 |
6 | public class TrafficMonitorStream(IHttpResponseBodyFeature httpResponseBodyFeature) : Stream, IHttpResponseBodyFeature
7 | {
8 | public int Bytes { get; set; }
9 |
10 | public override bool CanRead => BaseStream.CanRead;
11 |
12 | public override bool CanSeek => BaseStream.CanSeek;
13 |
14 | public override bool CanWrite => BaseStream.CanWrite;
15 |
16 | public override long Length => BaseStream.Length;
17 |
18 | public override long Position { get => BaseStream.Position; set => BaseStream.Position = value; }
19 |
20 | public Stream BaseStream { get; set; } = httpResponseBodyFeature.Stream;
21 |
22 | public Stream Stream => this;
23 |
24 | private PipeWriter? _pipeAdapter;
25 |
26 | public PipeWriter Writer
27 | {
28 | get
29 | {
30 | _pipeAdapter ??= PipeWriter.Create(Stream, new StreamPipeWriterOptions(leaveOpen: true));
31 | return _pipeAdapter;
32 | }
33 | }
34 |
35 | public Task CompleteAsync() => httpResponseBodyFeature.CompleteAsync();
36 |
37 | public void DisableBuffering() => httpResponseBodyFeature.DisableBuffering();
38 |
39 | public override void Flush() => BaseStream.Flush();
40 |
41 | public override int Read(byte[] buffer, int offset, int count) => BaseStream.Read(buffer, offset, count);
42 |
43 | public override long Seek(long offset, SeekOrigin origin) => BaseStream.Seek(offset, origin);
44 |
45 | public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellationToken = default)
46 | {
47 | return httpResponseBodyFeature.SendFileAsync(path, offset, count, cancellationToken);
48 | }
49 |
50 | public override void SetLength(long value) => BaseStream.SetLength(value);
51 |
52 | public Task StartAsync(CancellationToken cancellationToken = default) => httpResponseBodyFeature.StartAsync(cancellationToken);
53 |
54 | public override void Write(byte[] buffer, int offset, int count)
55 | {
56 | Bytes += count;
57 | BaseStream.Write(buffer, offset, count);
58 | }
59 |
60 |
61 | public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
62 | {
63 | Bytes += count;
64 | await WriteAsync(buffer.AsMemory(offset, count), cancellationToken);
65 | }
66 |
67 | public override async ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken)
68 | {
69 | Bytes += buffer.Length;
70 | await BaseStream.WriteAsync(buffer, cancellationToken);
71 | }
72 |
73 | public override void Write(ReadOnlySpan buffer)
74 | {
75 | Bytes += buffer.Length;
76 | BaseStream.Write(buffer);
77 | }
78 |
79 | public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state)
80 | {
81 | Bytes += count;
82 | return BaseStream.BeginRead(buffer, offset, count, callback, state);
83 | }
84 |
85 | public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state)
86 | {
87 | Bytes += count;
88 | return BaseStream.BeginWrite(buffer, offset, count, callback, state);
89 | }
90 |
91 | public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken)
92 | {
93 | return BaseStream.CopyToAsync(destination, bufferSize, cancellationToken);
94 | }
95 |
96 | public override void CopyTo(Stream destination, int bufferSize)
97 | {
98 | BaseStream.CopyTo(destination, bufferSize);
99 | }
100 |
101 | public override Task FlushAsync(CancellationToken cancellationToken) => BaseStream.FlushAsync(cancellationToken);
102 |
103 | public override void WriteByte(byte value)
104 | {
105 | Bytes += value;
106 | BaseStream.WriteByte(value);
107 | }
108 |
109 | public override void EndWrite(IAsyncResult asyncResult) => BaseStream.EndWrite(asyncResult);
110 | }
--------------------------------------------------------------------------------
/DstServerQuery.Web/Services/TrafficRateLimiter/TrafficRateLimit.cs:
--------------------------------------------------------------------------------
1 | using System.Text.RegularExpressions;
2 |
3 | namespace DstServerQuery.Web.Services.TrafficRateLimiter;
4 |
5 | ///
6 | /// 在秒内, 只能请求字节
7 | ///
8 | public partial class TrafficRateLimit
9 | {
10 | private string window = null!;
11 | public string Window
12 | {
13 | get => window;
14 | set
15 | {
16 | ArgumentException.ThrowIfNullOrEmpty(value);
17 | var match = ValueRegex().Match(value);
18 | if (!match.Success)
19 | throw new ArgumentException(null, nameof(value));
20 |
21 | double v = double.Parse(match.Groups["value"].Value);
22 | string unit = match.Groups["unit"].Value;
23 |
24 | WindowSec = (int)(unit switch
25 | {
26 | "s" or "sec" => v,
27 | "m" or "minute" => v * 60,
28 | "h" or "hour" => v * 3600,
29 | "d" or "day" or "days" => v * 3600 * 24,
30 | _ => v,
31 | });
32 | window = value;
33 | }
34 | }
35 |
36 |
37 | public int WindowSec { get; private set; }
38 |
39 | private string traffic = null!;
40 |
41 | public required string Traffic
42 | {
43 | get => traffic;
44 | set
45 | {
46 | ArgumentException.ThrowIfNullOrEmpty(value);
47 | var match = ValueRegex().Match(value);
48 | if (!match.Success)
49 | throw new ArgumentException(null, nameof(value));
50 |
51 | double v = double.Parse(match.Groups["value"].Value);
52 | string unit = match.Groups["unit"].Value;
53 |
54 | TrafficBytes = (int)(unit switch
55 | {
56 | "b" or "byte" or "bytes" => v,
57 | "kb" or "k" => v * 1024,
58 | "mb" or "m" => v * 1024 * 1024,
59 | _ => v,
60 | });
61 |
62 | traffic = value;
63 | }
64 | }
65 |
66 | public int TrafficBytes { get; private set; }
67 |
68 | [GeneratedRegex(@"^(?[0-9\.]+)(?.*)$")]
69 | private static partial Regex ValueRegex();
70 | }
--------------------------------------------------------------------------------
/DstServerQuery.Web/Services/TrafficRateLimiter/TrafficRateLimitOptions.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Concurrent;
2 |
3 | namespace DstServerQuery.Web.Services.TrafficRateLimiter;
4 |
5 | public class TrafficRateLimitOptions
6 | {
7 | public ConcurrentDictionary> Users { get; } = new();
8 |
9 | public int StatusCode { get; set; } = 429;
10 | public int MaxQueue { get; set; } = 1000;
11 |
12 | public string[] IPHeader { get; set; } = [];
13 | public TrafficRateLimit[] TrafficAny { get; set; } = []; // 任何IP的流量限制器
14 | public Dictionary TrafficTargets { get; set; } = []; // 特定IP速率限制器
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/DstServerQuery.Web/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/DstServerQuery.Web/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | //"Logging": {
3 | // "LogLevel": {
4 | // "Default": "Information",
5 | // "Microsoft.AspNetCore": "Warning"
6 | // }
7 | //},
8 |
9 | "Serilog": {
10 | "MinimumLevel": {
11 | "Default": "Information",
12 | "Override": { //系统日志最小记录级别
13 | "Microsoft": "Information",
14 | "Microsoft.EntityFrameworkCore": "Warning"
15 | }
16 | },
17 | "WriteTo": [
18 | //{
19 | // "Name": "Async",
20 | // "Args": {
21 | // "configure": [
22 | // {
23 | // "Name": "File",
24 | // "Args": {
25 | // "path": "Logs/log.log",
26 | // "rollingInterval": "Day"
27 | // }
28 | // }
29 | // ]
30 | // }
31 | //}
32 | ]
33 | },
34 |
35 | //"Urls": "http://127.0.0.1:3000",
36 | "AllowedHosts": "*",
37 | "SqlType": "Sqlite", // None, SqlServer MySql PostgreSql Sqlite
38 | "ConnectionStrings": {
39 | "SqlServer": "Persist Security Info=True;User ID=SA;Database=Dst;Server=localhost;Password=123456;MultipleActiveResultSets=True;TrustServerCertificate=True",
40 | "MySql": "Server=localhost;Database=Dst;UserID=root;Password=123456;AllowLoadLocalInfile=true",
41 | "PostgreSql": "Host=localhost;Port=5432;Database=Dst;Username=postgres;Password=123456",
42 | "Sqlite": "Data Source=Dst.db"
43 | },
44 | "DstConfig": {
45 | "Token": "LobbyListings_KU_XXXXXXXX_SpecialToken_00000000000000000000000000000000", // 开服令牌
46 | "LobbyProxyTemplate": "https://lobby-v2-cdn.klei.com/{region}-{platform}.json.gz", // 将替换{region}和{platform}
47 | "ServerUpdateInterval": 10, // 服务器更新间隔(秒)
48 | "ServerDetailsUpdateInterval": 10, // 详细信息更新间隔(秒)
49 | "HistoryLiteUpdateInterval": 10, // 历史记录更新间隔(秒)
50 | "HistoryDetailsUpdateInterval": 10, // 详细信息历史记录更新间隔(秒)
51 | "IsDisabledInsertDatabase": true, // 是否禁用数据库插入, 但是依旧会连接SqlType的数据库
52 | "UpdateThreads": 6, // 详细信息更新的线程数量
53 | "IsCountFromPlayers": false,
54 | "HistoryExpiration": 24 // 历史记录过期删除时间(小时)
55 | },
56 | "Steam": { // Steam配置
57 | "SteampoweredApiProxy": "https://api.steampowered.com/",
58 | "WebApiKey": null
59 | },
60 | "DstVersionService": { // 饥荒版本服务
61 | "IsEnabled": true, // 是否启用服务
62 | "DefaultVersion": null, // 首次读取的饥荒版本
63 | "IsDisabledUpdate": false // 是否禁用'获取更新'
64 | },
65 | "DstModsFileService": { // 饥荒Mods服务
66 | "IsEnabled": false, // 是否启用
67 | "RootPath": "./mods", // Mods文件储存目录
68 | "FileUrlProxy": null // template: {url}
69 | },
70 | "GeoLite2Path": "GeoLite2-City.mmdb",
71 | "EnabledCommandLine": false,
72 | "ApiDocumentBaseUrl": null, // API文档的URL前缀
73 |
74 | // 速率限制
75 | "IpRateLimiting": {
76 | "EnableEndpointRateLimiting": true, // false则全局将应用限制,并且仅应用具有作为端点的规则* ,true则限制将应用于每个端点,如{HTTP_Verb}{PATH}
77 | "StackBlockedRequests": false, // false则拒绝的API调用不会添加到调用次数计数器上
78 | "RealIpHeader": "X-Real-IP", // 表示获取用户端的真实IP, 可能是X-Real-IP或者X-Forwarded-For
79 | "ClientIdHeader": "X-ClientId",
80 | "HttpStatusCode": 429,
81 | "QuotaExceededResponse": {
82 | "Content": "{{\"Code\":429,\"Error\":\"Too Many Requests\"}}",
83 | "ContentType": "application/json",
84 | "StatusCode": 429
85 | },
86 | //"IpWhitelist": [ "127.0.0.1", "::1/10", "192.168.0.0/24" ],
87 | "EndpointWhitelist": [],
88 | "ClientWhitelist": [],
89 | "GeneralRules": [
90 | {
91 | "Endpoint": "*",
92 | "Period": "1s",
93 | "Limit": 2
94 | },
95 | {
96 | "Endpoint": "*",
97 | "Period": "60s",
98 | "Limit": 20
99 | }
100 | ]
101 | },
102 | // 特定IP速率限制
103 | "IpRateLimitPolicies": {
104 | "IpRules": [
105 | {
106 | "Ip": "1.1.1.1",
107 | "Rules": [
108 | {
109 | "Endpoint": "*",
110 | "Period": "1s",
111 | "Limit": 1
112 | },
113 | {
114 | "Endpoint": "*",
115 | "Period": "60s",
116 | "Limit": 10
117 | }
118 | ]
119 | }
120 | ]
121 | },
122 |
123 | // 流量速率限制
124 | "TrafficRateLimit": {
125 | "IPHeader": [ "X-Forwarded-For" ],
126 | "Any": [
127 | {
128 | "Traffic": "20mb",
129 | "Window": "120s"
130 | }
131 | ],
132 | "Targets": {
133 | "127.0.0.1": [
134 | {
135 | "Traffic": "3mb",
136 | "Window": "10s"
137 | }
138 | ]
139 | }
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/DstServerQuery.Web/libman.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "1.0",
3 | "defaultProvider": "cdnjs",
4 | "libraries": []
5 | }
--------------------------------------------------------------------------------
/DstServerQuery.Web/migrate-dst.ps1:
--------------------------------------------------------------------------------
1 | param(
2 | [string]$databaseType,
3 | [string]$migrationName
4 | )
5 |
6 | if (-not $databaseType) {
7 | Write-Host "请输入数据库类型"
8 | exit 1
9 | }
10 | if (-not $migrationName) {
11 | Write-Host "请输入迁移名称"
12 | exit 1
13 | }
14 |
15 | if ($databaseType -ieq "SqlServer") {
16 | dotnet ef migrations add $migrationName --context SqlServerDstDbContext --output-dir Migrations/SqlServer -- SqlType=SqlServer
17 | }
18 | elseif ($databaseType -ieq "MySql") {
19 | dotnet ef migrations add $migrationName --context MySqlDstDbContext --output-dir Migrations/MySql -- SqlType=MySql
20 | }
21 | elseif ($databaseType -ieq "Sqlite") {
22 | dotnet ef migrations add $migrationName --context SqliteDstDbContext --output-dir Migrations/Sqlite -- SqlType=Sqlite
23 | }
24 | elseif ($databaseType -ieq "PostgreSql") {
25 | dotnet ef migrations add $migrationName --context PostgreSqlDstDbContext --output-dir Migrations/PostgreSql -- SqlType=PostgreSql
26 | }
27 | else {
28 | Write-Host "数据库类型错误"
29 | }
30 |
--------------------------------------------------------------------------------
/DstServerQuery.Web/wwwroot/doc/swagger_ext.js:
--------------------------------------------------------------------------------
1 | function sendScroll() {
2 | window.parent.postMessage({
3 | type: "scroll",
4 | scroolWidth: document.body.scrollWidth,
5 | scrollHeight: document.body.scrollHeight,
6 | }, "*");
7 | }
8 |
9 | let lastHeight = 0;
10 | setInterval(() => {
11 | let currentHeight = document.body.scrollHeight;
12 | if (lastHeight != currentHeight) {
13 | lastHeight = currentHeight;
14 | sendScroll();
15 | }
16 | }, 100);
--------------------------------------------------------------------------------
/DstServerQuery.slnx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/DstServerQuery/Converters/GameModeConverter.cs:
--------------------------------------------------------------------------------
1 | using DstServerQuery.Helpers;
2 | using DstServerQuery.Models.Lobby;
3 | using System.Text.Json;
4 | using System.Text.Json.Serialization;
5 |
6 | namespace DstServerQuery.Converters;
7 |
8 | public class GameModeConverter : JsonConverter
9 | {
10 | public static ConcurrentStringCacheDictionary Cache { get; } = new();
11 |
12 | public override LobbyGameMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
13 | {
14 | if (reader.TokenType != JsonTokenType.String)
15 | throw new JsonException("不是一个字符串");
16 |
17 | if (reader.ValueSpan.Length == 0)
18 | return new(string.Empty);
19 |
20 | return new(Cache.GetOrAdd(reader.ValueSpan));
21 | }
22 |
23 | public override void Write(Utf8JsonWriter writer, LobbyGameMode value, JsonSerializerOptions options)
24 | {
25 | writer.WriteStringValue(value.Value);
26 | }
27 | }
28 |
29 | public class GameModeWithTranslateConverter : JsonConverter
30 | {
31 | public override LobbyGameMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
32 | {
33 | throw new NotSupportedException();
34 | }
35 |
36 | public override void Write(Utf8JsonWriter writer, LobbyGameMode value, JsonSerializerOptions options)
37 | {
38 | writer.WriteStringValue(DstEnumText.Instance.GetFromString(value.Value ?? "", value.Value ?? ""));
39 | }
40 | }
--------------------------------------------------------------------------------
/DstServerQuery/Converters/IPAddressInfoConverter.cs:
--------------------------------------------------------------------------------
1 | using DstServerQuery.Helpers;
2 | using DstServerQuery.Models;
3 | using System.Diagnostics;
4 | using System.Text.Json;
5 | using System.Text.Json.Serialization;
6 |
7 | namespace DstServerQuery.Converters;
8 |
9 | public class IPAddressStringConverter : JsonConverter
10 | {
11 | public override void Write(Utf8JsonWriter writer, IPAddressInfo value, JsonSerializerOptions options)
12 | {
13 | JsonSerializer.Serialize(writer, value.IP);
14 | }
15 |
16 | public override IPAddressInfo? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
17 | {
18 | Debug.Assert(reader.TokenType != JsonTokenType.Null);
19 |
20 | if (reader.ValueSpan.SequenceEqual("127.0.0.1"u8))
21 | {
22 | return DstConverterHelper.ParseAddress("127.0.0.1");
23 | }
24 |
25 | return DstConverterHelper.ParseAddress(reader.GetString()!);
26 | }
27 | }
--------------------------------------------------------------------------------
/DstServerQuery/Converters/IdCacheConverter.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json;
2 | using System.Text.Json.Serialization;
3 |
4 | namespace DstServerQuery.Converters;
5 |
6 | public class IdCacheConverter : JsonConverter
7 | {
8 | public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
9 | {
10 | if (reader.TokenType == JsonTokenType.Null)
11 | return null;
12 |
13 | if (reader.TokenType != JsonTokenType.String)
14 | {
15 | throw new JsonException("不是一个string");
16 | }
17 |
18 | if (reader.ValueSpan.Length == 0)
19 | return string.Empty;
20 |
21 | if (reader.ValueSpan.Length == 1)
22 | {
23 | return (char)reader.ValueSpan[0] switch
24 | {
25 | '0' => "0",
26 | '1' => "1",
27 | '2' => "2",
28 | '3' => "3",
29 | '4' => "4",
30 | '5' => "5",
31 | '6' => "6",
32 | '7' => "7",
33 | '8' => "8",
34 | '9' => "9",
35 | _ => reader.GetString(),
36 | };
37 | }
38 | else if (reader.ValueSpan.Length == 5)
39 | {
40 | if (reader.ValueSpan.SequenceEqual("10010"u8))
41 | {
42 | return "10010";
43 | }
44 | }
45 |
46 | return reader.GetString();
47 | }
48 |
49 | public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)
50 | {
51 | writer.WriteStringValue(value);
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/DstServerQuery/Converters/IntentConverter.cs:
--------------------------------------------------------------------------------
1 | using DstServerQuery.Helpers;
2 | using DstServerQuery.Models.Lobby;
3 | using System.Text.Json;
4 | using System.Text.Json.Serialization;
5 |
6 | namespace DstServerQuery.Converters;
7 |
8 | public class IntentConverter : JsonConverter
9 | {
10 | public override LobbyIntent Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
11 | {
12 | if (reader.TokenType != JsonTokenType.String)
13 | throw new JsonException("不是一个字符串");
14 |
15 | if (reader.ValueSpan.Length == 0)
16 | return new(string.Empty);
17 |
18 | return 0 switch
19 | {
20 | _ when reader.ValueSpan.SequenceEqual("relaxed"u8) => new("relaxed"),
21 | _ when reader.ValueSpan.SequenceEqual("endless"u8) => new("endless"),
22 | _ when reader.ValueSpan.SequenceEqual("survival"u8) => new("survival"),
23 | _ when reader.ValueSpan.SequenceEqual("wilderness"u8) => new("wilderness"),
24 | _ when reader.ValueSpan.SequenceEqual("cooperative"u8) => new("cooperative"),
25 | _ when reader.ValueSpan.SequenceEqual("lightsout"u8) => new("lightsout"),
26 | _ when reader.ValueSpan.SequenceEqual("oceanfishing"u8) => new("oceanfishing"),
27 | _ => new(reader.GetString()),
28 | };
29 | }
30 |
31 | public override void Write(Utf8JsonWriter writer, LobbyIntent value, JsonSerializerOptions options)
32 | {
33 | writer.WriteStringValue(value.Value);
34 | }
35 | }
36 |
37 | public class IntentWithTranslateConverter : JsonConverter
38 | {
39 | public override LobbyIntent Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
40 | {
41 | throw new NotSupportedException();
42 | }
43 |
44 | public override void Write(Utf8JsonWriter writer, LobbyIntent value, JsonSerializerOptions options)
45 | {
46 | writer.WriteStringValue(DstEnumText.Instance.GetFromString(value.Value ?? "", value.Value ?? ""));
47 | }
48 | }
--------------------------------------------------------------------------------
/DstServerQuery/Converters/LobbyDaysInfoConverter.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json;
2 | using System.Text.Json.Serialization;
3 | using DstServerQuery.Helpers;
4 | using DstServerQuery.Models;
5 |
6 | namespace DstServerQuery.Converters;
7 |
8 | public class LobbyDaysInfoConverter : JsonConverter
9 | {
10 | public override LobbyDaysInfo? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
11 | {
12 | if (reader.TokenType is JsonTokenType.Null)
13 | {
14 | return null;
15 | }
16 | if (reader.TokenType is not JsonTokenType.String)
17 | {
18 | throw new Exception("不是一个字符串");
19 | }
20 | if (reader.ValueSpan.Length <= 128)
21 | {
22 | Span buffer = stackalloc char[reader.ValueSpan.Length];
23 | var charsLen = reader.CopyString(buffer);
24 | if (DstConverterHelper.ParseDays(buffer[..charsLen]) is { } days)
25 | {
26 | return days;
27 | }
28 | else
29 | {
30 | using TempUtf8JsonString str = TempUtf8JsonString.From(reader);
31 | return DstConverterHelper.ParseDays(str.String);
32 | }
33 | }
34 | else
35 | {
36 | using TempUtf8JsonString str = TempUtf8JsonString.From(reader);
37 | if (DstConverterHelper.ParseDays(str.String.Span) is { } days)
38 | {
39 | return days;
40 | }
41 | else
42 | {
43 | return DstConverterHelper.ParseDays(str.String);
44 | }
45 | }
46 | }
47 | public override void Write(Utf8JsonWriter writer, LobbyDaysInfo value, JsonSerializerOptions options)
48 | {
49 | JsonSerializer.Serialize(writer, value, options);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/DstServerQuery/Converters/LobbyGuidConverter.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics;
2 | using System.Text.Json;
3 | using System.Text.Json.Serialization;
4 | using DstServerQuery.Models.Lobby;
5 |
6 | namespace DstServerQuery.Converters;
7 |
8 | internal class LobbyGuidConverter : JsonConverter
9 | {
10 | public override LobbyGuid Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
11 | {
12 | if (reader.TokenType != JsonTokenType.String)
13 | {
14 | throw new JsonException();
15 | }
16 |
17 | // 8451361192076405236
18 |
19 | Debug.Assert(reader.ValueSpan.Length <= 20);
20 | return new LobbyGuid(reader.ValueSpan);
21 | }
22 |
23 | public override void Write(Utf8JsonWriter writer, LobbyGuid value, JsonSerializerOptions options)
24 | {
25 | Span output = stackalloc byte[20];
26 | value.Value.TryFormat(output, out var writtenLen);
27 | writer.WriteStringValue(output[..writtenLen]);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/DstServerQuery/Converters/LobbyModsInfoConverter.cs:
--------------------------------------------------------------------------------
1 | using System.Buffers;
2 | using System.Diagnostics;
3 | using System.Text.Json;
4 | using System.Text.Json.Serialization;
5 | using System.Text.RegularExpressions;
6 | using DstServerQuery.Helpers;
7 | using DstServerQuery.Models;
8 |
9 | namespace DstServerQuery.Converters;
10 |
11 | /*
12 | * [
13 | * "workshop-xxxx", // Id
14 | * "Name", // Name
15 | * 1.1.0, // NewVersion
16 | * 1.0.0, // CurrentVersion
17 | * false // IsClientDownload
18 | * ]
19 | */
20 |
21 | public partial class LobbyModsInfoConverter : JsonConverter
22 | {
23 | public static ConcurrentStringCacheDictionary ModsVersionCache { get; } = new();
24 | public static ConcurrentStringCacheDictionary ModsNameCache { get; } = new();
25 |
26 | public override LobbyModInfo[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
27 | {
28 | if (reader.TokenType is JsonTokenType.Null)
29 | {
30 | return null;
31 | }
32 |
33 | List list = new();
34 | LobbyModInfo current = null!;
35 | int i = 0;
36 | if (reader.TokenType is not JsonTokenType.StartArray)
37 | {
38 | return null;
39 | }
40 |
41 | Span buffer = stackalloc char[256]; // Name,NewVersion,CurrentVersion buffer
42 |
43 | try
44 | {
45 | while (reader.Read())
46 | {
47 | if (reader.TokenType == JsonTokenType.EndArray)
48 | {
49 | break;
50 | }
51 |
52 | if (i % 5 == 0)
53 | {
54 | current = new();
55 | list.Add(current);
56 | }
57 |
58 | if (i % 5 == 0)
59 | {
60 | using var workshopString = TempUtf8JsonString.From(reader);
61 | const string WorkshopPrefix = "workshop-";
62 | if (workshopString.String.Span.StartsWith(WorkshopPrefix))
63 | {
64 | var idString = workshopString.String.Span[WorkshopPrefix.Length..];
65 | if (long.TryParse(idString, out var id))
66 | {
67 | current.Id = id;
68 | }
69 | else
70 | {
71 | current.Id = 0;
72 | }
73 | }
74 | else
75 | {
76 | current.Id = 0;
77 | }
78 | }
79 | else if (i % 5 == 1)
80 | {
81 | Debug.Assert(reader.TokenType == JsonTokenType.String);
82 | if (reader.TokenType is JsonTokenType.String)
83 | {
84 | Debug.Assert(reader.ValueSpan.Length <= buffer.Length);
85 | var len = reader.CopyString(buffer);
86 | current.Name = ModsNameCache.GetOrAdd(buffer[..len]);
87 | }
88 | }
89 | else if (i % 5 == 2)
90 | {
91 | if (reader.TokenType is JsonTokenType.Null)
92 | {
93 | current.NewVersion = null;
94 | }
95 | else if (reader.TokenType is JsonTokenType.String)
96 | {
97 | Debug.Assert(reader.ValueSpan.Length <= 128);
98 | var len = reader.CopyString(buffer);
99 | var cachedNewVersion = ModsVersionCache.GetOrAdd(buffer[..len]);
100 | current.NewVersion = cachedNewVersion;
101 | }
102 | else
103 | {
104 | list.RemoveAt(list.Count - 1);
105 | i -= i % 5 + 5;
106 | continue;
107 | }
108 | }
109 | else if (i % 5 == 3)
110 | {
111 | if (reader.TokenType is JsonTokenType.Null)
112 | {
113 | current.CurrentVersion = null;
114 | }
115 | else if (reader.TokenType is JsonTokenType.String)
116 | {
117 | Debug.Assert(reader.ValueSpan.Length <= 128);
118 | var len = reader.CopyString(buffer);
119 | var cachedCurrentVersion = ModsVersionCache.GetOrAdd(buffer[..len]);
120 | current.CurrentVersion = cachedCurrentVersion;
121 | }
122 | else
123 | {
124 | list.RemoveAt(list.Count - 1);
125 | i = i - i % 5 + 5;
126 | continue;
127 | }
128 | }
129 | else if (i % 5 == 4)
130 | {
131 | if (reader.TokenType is not (JsonTokenType.True or JsonTokenType.False))
132 | {
133 | Debugger.Break();
134 | }
135 | current.IsClientDownload = reader.GetBoolean();
136 | }
137 | i++;
138 | }
139 | }
140 | catch (Exception ex)
141 | {
142 | Debugger.Break();
143 | throw;
144 | }
145 |
146 | return list.ToArray();
147 | }
148 |
149 | public override void Write(Utf8JsonWriter writer, LobbyModInfo[] value, JsonSerializerOptions options)
150 | {
151 | JsonSerializer.Serialize(writer, value, options);
152 | }
153 |
154 | }
155 |
--------------------------------------------------------------------------------
/DstServerQuery/Converters/LobbyNumberIdConverter.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json;
2 | using System.Text.Json.Serialization;
3 | using DstServerQuery.Models.Lobby;
4 |
5 | namespace DstServerQuery.Converters;
6 |
7 | public class LobbyNumberIdConverter : JsonConverter
8 | {
9 | public override LobbyNumberId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
10 | {
11 | if(reader.TokenType != JsonTokenType.String)
12 | {
13 | throw new JsonException("不是一个字符串");
14 | }
15 |
16 | if (long.TryParse(reader.ValueSpan, out var number))
17 | {
18 | return new LobbyNumberId(number);
19 | }
20 | else
21 | {
22 | throw new JsonException("不是一个数字");
23 | }
24 | }
25 | public override void Write(Utf8JsonWriter writer, LobbyNumberId value, JsonSerializerOptions options)
26 | {
27 | Span output = stackalloc byte[20];
28 | value.Value.TryFormat(output, out var writtenLen);
29 | writer.WriteStringValue(output[..writtenLen]);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/DstServerQuery/Converters/LobbyPlayersInfoConverter.cs:
--------------------------------------------------------------------------------
1 | using DstServerQuery.Helpers;
2 | using DstServerQuery.Models;
3 | using System.Collections.Frozen;
4 | using System.Text.Json;
5 | using System.Text.Json.Serialization;
6 |
7 | namespace DstServerQuery.Converters;
8 |
9 | public class LobbyPlayersInfoConverter : JsonConverter
10 | {
11 | private static ReadOnlySpan _emptyUtf8 => "return { }"u8;
12 |
13 | public override LobbyPlayerInfo[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
14 | {
15 | if (reader.TokenType is JsonTokenType.Null)
16 | {
17 | return null;
18 | }
19 |
20 | if (reader.TokenType is not JsonTokenType.String)
21 | {
22 | throw new JsonException("不是一个字符串");
23 | }
24 |
25 | if (reader.ValueSpan.SequenceEqual(_emptyUtf8))
26 | {
27 | return [];
28 | }
29 |
30 | if (reader.ValueSpan.Length <= 256)
31 | {
32 | Span buffer = stackalloc char[reader.ValueSpan.Length];
33 | var charsLen = reader.CopyString(buffer);
34 | if (DstConverterHelper.ParsePlayers(buffer[..charsLen]) is { } players)
35 | {
36 | return players;
37 | }
38 | else
39 | {
40 | using TempUtf8JsonString str = TempUtf8JsonString.From(reader);
41 | return DstConverterHelper.ParsePlayers(str.String);
42 | }
43 | }
44 | else
45 | {
46 | using TempUtf8JsonString str = TempUtf8JsonString.From(reader);
47 | if (DstConverterHelper.ParsePlayers(str.String.Span) is { } players)
48 | {
49 | return players;
50 | }
51 | else
52 | {
53 | return DstConverterHelper.ParsePlayers(str.String);
54 | }
55 | }
56 | }
57 |
58 | public override void Write(Utf8JsonWriter writer, LobbyPlayerInfo[] value, JsonSerializerOptions options)
59 | {
60 | JsonSerializer.Serialize(writer, value, options);
61 | //writer.WriteStartArray();
62 | //foreach (var item in value)
63 | //{
64 | // writer.WriteStartObject();
65 | // writer.WriteString("Name", item.Name);
66 | // writer.WriteString("Color", item.Color);
67 | // writer.WriteNumber("EventLevel", item.EventLevel);
68 | // writer.WriteString("NetId", item.NetId);
69 | // writer.WriteString("Prefab", item.Prefab);
70 | // writer.WriteEndObject();
71 | //}
72 | //writer.WriteEndArray();
73 | }
74 | }
75 |
76 | public class PlayersInfoWitTranslateConverter : JsonConverter
77 | {
78 | public static readonly FrozenDictionary PrefabTranslations = new Dictionary
79 | {
80 | {"waxwell", "麦斯威尔"},
81 | {"wendy", "温蒂"},
82 | {"wes", "韦斯"},
83 | {"willow", "薇洛"},
84 | {"wilson", "威尔逊"},
85 | {"winona", "薇诺娜"},
86 | {"wolfgang", "沃尔夫冈"},
87 | {"woodie", "伍迪"},
88 | {"wortox", "沃拓克斯" }, //恶魔
89 | {"wx-78", "WX-78"},
90 | {"wickerbottom", "薇克巴顿"},
91 | {"wathgrithr", "女武神"},
92 | {"wanda", "旺达"},
93 | {"wormwood", "沃姆伍德" }, //植物人
94 | {"walter", "沃尔特"},
95 | {"webber", "韦伯"},
96 | {"wurt", "沃特"}, //鱼人
97 | {"warly", "沃利" },
98 |
99 | { "yangjian", "杨戬" }, //神话
100 | { "monkey_king", "孙悟空" }, //神话
101 | { "myth_yutu", "玉兔" }, //神话
102 | { "white_bone", "白骨夫人" }, //神话
103 |
104 | { "xuaner", "璇儿" }, //璇儿
105 | }.ToFrozenDictionary();
106 |
107 |
108 | public override LobbyPlayerInfo[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
109 | {
110 | throw new NotSupportedException();
111 | }
112 |
113 | public override void Write(Utf8JsonWriter writer, LobbyPlayerInfo[] value, JsonSerializerOptions options)
114 | {
115 | writer.WriteStartArray();
116 | foreach (var item in value)
117 | {
118 | writer.WriteStartObject();
119 | writer.WriteString("Name", item.Name);
120 | writer.WriteString("Color", item.Color);
121 | writer.WriteNumber("EventLevel", item.EventLevel);
122 | writer.WriteString("NetId", item.NetId);
123 | if (PrefabTranslations.TryGetValue(item.Prefab, out var translation))
124 | {
125 | writer.WriteString("Prefab", translation);
126 | }
127 | else
128 | {
129 | writer.WriteString("Prefab", item.Prefab);
130 | }
131 | writer.WriteEndObject();
132 | }
133 | writer.WriteEndArray();
134 | }
135 | }
--------------------------------------------------------------------------------
/DstServerQuery/Converters/LobbySeasonConverter.cs:
--------------------------------------------------------------------------------
1 | using DstServerQuery.Helpers;
2 | using DstServerQuery.Models.Lobby;
3 | using System.Text.Json;
4 | using System.Text.Json.Serialization;
5 |
6 | namespace DstServerQuery.Converters;
7 |
8 | public class LobbySeasonConverter : JsonConverter
9 | {
10 | public override LobbySeason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
11 | {
12 | if (reader.TokenType != JsonTokenType.String)
13 | throw new JsonException("不是一个字符串");
14 |
15 | if (reader.ValueSpan.Length == 0)
16 | return new(string.Empty);
17 |
18 | return 0 switch
19 | {
20 | _ when reader.ValueSpan.SequenceEqual("spring"u8) => new("spring"),
21 | _ when reader.ValueSpan.SequenceEqual("summer"u8) => new("summer"),
22 | _ when reader.ValueSpan.SequenceEqual("autumn"u8) => new("autumn"),
23 | _ when reader.ValueSpan.SequenceEqual("winter"u8) => new("winter"),
24 | _ => new(reader.GetString()),
25 | };
26 | }
27 | public override void Write(Utf8JsonWriter writer, LobbySeason value, JsonSerializerOptions options)
28 | {
29 | writer.WriteStringValue(value.Value);
30 | }
31 | }
32 |
33 | public class SeasonWithTranslateConverter : JsonConverter
34 | {
35 | public override LobbySeason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
36 | {
37 | throw new NotSupportedException();
38 | }
39 | public override void Write(Utf8JsonWriter writer, LobbySeason value, JsonSerializerOptions options)
40 | {
41 | writer.WriteStringValue(DstEnumText.Instance.GetFromString(value.Value ?? "", value.Value ?? ""));
42 | }
43 | }
--------------------------------------------------------------------------------
/DstServerQuery/Converters/LobbySessionIdConverter.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics;
2 | using System.Text.Json;
3 | using System.Text.Json.Serialization;
4 | using DstServerQuery.Helpers;
5 | using DstServerQuery.Models.Lobby;
6 |
7 | namespace DstServerQuery.Converters;
8 |
9 | public class LobbySessionIdConverter : JsonConverter
10 | {
11 | public override LobbySessionId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
12 | {
13 | if (reader.TokenType != JsonTokenType.String)
14 | {
15 | throw new JsonException();
16 | }
17 |
18 | // "0E516F29EF6D3712"
19 | // "05375D373EC81FY3"
20 |
21 | Debug.Assert(reader.ValueSpan.Length == 16);
22 | return new LobbySessionId(reader.ValueSpan);
23 | }
24 |
25 | public override void Write(Utf8JsonWriter writer, LobbySessionId value, JsonSerializerOptions options)
26 | {
27 | Span hex = stackalloc char[value.Value.Length];
28 | for (var i = 0; i < value.Value.Length; i++)
29 | {
30 | byte b = value.Value[i];
31 | hex[i] = (char)b;
32 | }
33 | writer.WriteStringValue(hex);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/DstServerQuery/Converters/LobbySteamIdConverter.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics;
2 | using System.Text.Json;
3 | using System.Text.Json.Serialization;
4 | using DstServerQuery.Models.Lobby;
5 |
6 | namespace DstServerQuery.Converters;
7 |
8 | internal class LobbySteamIdConverter : JsonConverter
9 | {
10 | public override LobbySteamId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
11 | {
12 | if (reader.TokenType != JsonTokenType.String)
13 | {
14 | throw new JsonException();
15 | }
16 |
17 | // 90243943705555997
18 | // P:48001167358525925
19 | // N:bVUBL4btQjQBAAAAAAAAAAE
20 |
21 | var valueSpan = reader.ValueSpan;
22 | if (valueSpan.IndexOf((byte)':') is int index and not -1)
23 | {
24 | valueSpan = valueSpan[(index + 1)..];
25 | }
26 | return new LobbySteamId(valueSpan);
27 | }
28 |
29 | public override void Write(Utf8JsonWriter writer, LobbySteamId value, JsonSerializerOptions options)
30 | {
31 | if (value.String is not null)
32 | {
33 | writer.WriteStringValue(value.String);
34 | return;
35 | }
36 | Span output = stackalloc byte[20];
37 | value.Value.TryFormat(output, out var writtenLen);
38 | writer.WriteStringValue(output[..writtenLen]); // "value.Value"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/DstServerQuery/Converters/LobbyTagsConverter.cs:
--------------------------------------------------------------------------------
1 | using System.Buffers;
2 | using System.Text;
3 | using System.Text.Json;
4 | using System.Text.Json.Serialization;
5 |
6 | namespace DstServerQuery.Converters;
7 |
8 | public class LobbyTagsConverter : JsonConverter
9 | {
10 | public static Dictionary Cache { get; } = new();
11 | public static Dictionary.AlternateLookup> CacheAlternateLookup { get; }
12 |
13 | static LobbyTagsConverter()
14 | {
15 | CacheAlternateLookup = Cache.GetAlternateLookup>();
16 | }
17 |
18 | public override string[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
19 | {
20 | var charsCount = Encoding.UTF8.GetCharCount(reader.ValueSpan);
21 | char[]? sharedBuffer = null;
22 | Span buffer = charsCount < 512 ? stackalloc char[charsCount] :
23 | (sharedBuffer = ArrayPool.Shared.Rent(charsCount));
24 |
25 | try
26 | {
27 | Encoding.UTF8.GetChars(reader.ValueSpan, buffer);
28 | var tagsCount = ((ReadOnlySpan)buffer).Count(',');
29 | var tags = new string[tagsCount + 1];
30 | int index = 0;
31 | foreach (var item in ((ReadOnlySpan)buffer).Split(','))
32 | {
33 | if (CacheAlternateLookup.TryGetValue(buffer[item], out var tag))
34 | {
35 | tags[index] = tag;
36 | }
37 | else
38 | {
39 | tag = buffer[item].ToString();
40 | Cache[tag] = tag;
41 | tags[index] = tag;
42 | }
43 | index++;
44 | }
45 | return tags;
46 | }
47 | finally
48 | {
49 | if (sharedBuffer is not null)
50 | {
51 | ArrayPool.Shared.Return(sharedBuffer);
52 | }
53 | }
54 | }
55 |
56 | public override void Write(Utf8JsonWriter writer, string[] value, JsonSerializerOptions options)
57 | {
58 | writer.WriteStartArray();
59 | foreach (var item in value)
60 | {
61 | writer.WriteStringValue(item);
62 | }
63 | writer.WriteEndArray();
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/DstServerQuery/Converters/LobbyWorldGenConverter.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json;
2 | using System.Text.Json.Nodes;
3 | using System.Text.Json.Serialization;
4 |
5 | namespace DstServerQuery.Converters;
6 |
7 | public class LobbyWorldGenConverter : JsonConverter