├── .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 8 | { 9 | public override object? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 10 | { 11 | //_ = JsonNode.Parse(ref reader); 12 | return null; 13 | } 14 | 15 | public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) 16 | { 17 | writer.WriteNullValue(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /DstServerQuery/Converters/LobbyWorldLevelConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | using DstServerQuery.Models; 4 | 5 | namespace DstServerQuery.Converters; 6 | 7 | public class LobbyWorldLevelConverter : JsonConverter 8 | { 9 | public override WorldLevelItem[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 10 | { 11 | var r = JsonSerializer.Deserialize>(ref reader); 12 | return r?.Values?.Select(v => WorldLevelItem.FromRaw(v)).ToArray(); 13 | } 14 | public override void Write(Utf8JsonWriter writer, WorldLevelItem[] value, JsonSerializerOptions options) 15 | { 16 | throw new NotImplementedException(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /DstServerQuery/DstServerQuery.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | true 8 | $(NoWarn);1591 9 | DstServerQuery 10 | 13.0 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /DstServerQuery/Helpers/ConcurrentStringCacheDictionary.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | using System.Collections.Concurrent; 3 | using System.Text; 4 | 5 | namespace DstServerQuery.Helpers; 6 | 7 | public class ConcurrentStringCacheDictionary 8 | { 9 | public ConcurrentDictionary Dictionary { get; } = new(); 10 | private readonly ConcurrentDictionary.AlternateLookup> _cacheAlternateLookup; 11 | 12 | public ConcurrentStringCacheDictionary() 13 | { 14 | _cacheAlternateLookup = Dictionary.GetAlternateLookup>(); 15 | } 16 | 17 | public string GetOrAdd(ReadOnlySpan chars) 18 | { 19 | if (_cacheAlternateLookup.TryGetValue(chars, out var str)) 20 | { 21 | return str; 22 | } 23 | else 24 | { 25 | str = chars.ToString(); 26 | Dictionary.TryAdd(str, str); 27 | return str; 28 | } 29 | } 30 | 31 | public string GetOrAdd(ReadOnlySpan bytes) 32 | { 33 | var charLen = Encoding.UTF8.GetCharCount(bytes); 34 | if (charLen < 512) 35 | { 36 | Span buffer = stackalloc char[charLen]; 37 | var writenLen = Encoding.UTF8.GetChars(bytes, buffer); 38 | if (_cacheAlternateLookup.TryGetValue(buffer[0..writenLen], out var tags)) 39 | { 40 | return tags; 41 | } 42 | else 43 | { 44 | tags = buffer[0..writenLen].ToString(); 45 | _cacheAlternateLookup.TryAdd(buffer[0..writenLen], buffer[0..writenLen].ToString()); 46 | return tags; 47 | } 48 | } 49 | else 50 | { 51 | var buffer = ArrayPool.Shared.Rent(charLen); 52 | try 53 | { 54 | var writenLen = Encoding.UTF8.GetChars(bytes, buffer); 55 | if (_cacheAlternateLookup.TryGetValue(buffer.AsSpan()[0..writenLen], out var tags)) 56 | { 57 | return tags; 58 | } 59 | else 60 | { 61 | tags = buffer.AsSpan()[0..writenLen].ToString(); 62 | _cacheAlternateLookup.TryAdd(buffer.AsSpan()[0..writenLen], buffer.AsSpan()[0..writenLen].ToString()); 63 | return tags; 64 | } 65 | } 66 | finally 67 | { 68 | ArrayPool.Shared.Return(buffer); 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /DstServerQuery/Helpers/DstEnumText.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using DstServerQuery.Models; 3 | 4 | namespace DstServerQuery.Helpers; 5 | 6 | /// 7 | /// 定义了枚举对应的文本 8 | /// 9 | public class DstEnumText : Dictionary 10 | { 11 | public static DstEnumText Instance => new Lazy(() => new DstEnumText() 12 | { 13 | { Day.@default, "默认" }, 14 | { Day.longday, "长白天" }, 15 | { Day.longdusk, "长黄昏" }, 16 | { Day.longnight, "长夜晚" }, 17 | { Day.noday, "无白天" }, 18 | { Day.nodusk, "无黄昏" }, 19 | { Day.nonight, "无夜晚" }, 20 | { Day.onlyday, "仅白天" }, 21 | { Day.onlydusk, "仅黄昏" }, 22 | { Day.onlynight, "仅夜晚" }, 23 | 24 | { Frequency.never, "无" }, 25 | { Frequency.rare, "很少" }, 26 | { Frequency.@default, "默认" }, 27 | { Frequency.often, "较多" }, 28 | { Frequency.always, "大量" }, 29 | 30 | { ExtraStartingItems._0, "总是" }, 31 | { ExtraStartingItems._5, "第5天后" }, 32 | { ExtraStartingItems._10, "第10天后" }, 33 | { ExtraStartingItems._15, "第15天后" }, 34 | { ExtraStartingItems._20, "第20天后" }, 35 | { ExtraStartingItems.none, "从不" }, 36 | 37 | { DropEverythingOnDespawn.@default, "总是" }, 38 | { DropEverythingOnDespawn.always, "所有" }, 39 | 40 | 41 | { Behaviour.never, "无" }, 42 | { Behaviour.@default, "默认" }, 43 | { Behaviour.always, "总是" }, 44 | 45 | { Speed.none, "无" }, 46 | { Speed.few, "慢" }, 47 | { Speed.@default, "默认" }, 48 | { Speed.many, "快" }, 49 | { Speed.max, "极快" }, 50 | 51 | { GrowthSpeed.never, "无" }, 52 | { GrowthSpeed.veryslow, "极慢" }, 53 | { GrowthSpeed.slow, "慢" }, 54 | { GrowthSpeed.@default, "默认" }, 55 | { GrowthSpeed.fast, "快" }, 56 | { GrowthSpeed.veryfast, "极快" }, 57 | 58 | { SpecialEvent.none, "无" }, 59 | { SpecialEvent.auto, "自动" }, 60 | { SpecialEvent.midsummer_cawnival, "盛夏鸦年华" }, 61 | { SpecialEvent.hallowed_nights, "万圣节" }, 62 | { SpecialEvent.winters_feast, "冬季盛宴" }, 63 | { SpecialEvent.year_of_the_gobbler, "火鸡之年" }, 64 | { SpecialEvent.year_of_the_varg, "座狼之年" }, 65 | { SpecialEvent.year_of_the_pig_king, "猪王之年" }, 66 | { SpecialEvent.year_of_the_carrat, "胡萝卜之年" }, 67 | { SpecialEvent.year_of_the_beefalo, "皮弗娄牛之年" }, 68 | { SpecialEvent.year_of_the_catcoon, "浣猫之年" }, 69 | 70 | { IsExist.never, "无" }, 71 | { IsExist.@default, "默认" }, 72 | 73 | { SeasonalDuration.noseason, "无" }, 74 | { SeasonalDuration.veryshortseason, "极短" }, 75 | { SeasonalDuration.shortseason, "短" }, 76 | { SeasonalDuration.@default, "默认" }, 77 | { SeasonalDuration.longseason, "长" }, 78 | { SeasonalDuration.verylongseason, "极长" }, 79 | { SeasonalDuration.random, "随机" }, 80 | 81 | 82 | 83 | { TaskSet.@default, "联机版" }, 84 | { TaskSet.classic, "经典" }, 85 | { TaskSet.cave_default, "洞穴" }, 86 | 87 | { WorldSize.small, "小" }, 88 | { WorldSize.medium, "中" }, 89 | { WorldSize.@default, "大" }, 90 | { WorldSize.huge, "巨大" }, 91 | 92 | { Branching.never, "从不" }, 93 | { Branching.least, "最少" }, 94 | { Branching.@default, "默认" }, 95 | { Branching.most, "最多" }, 96 | { Branching.random, "随机" }, 97 | 98 | { Quantity.never, "无" }, 99 | { Quantity.rare, "很少" }, 100 | { Quantity.uncommon, "较少" }, 101 | { Quantity.@default, "默认" }, 102 | { Quantity.often, "较多" }, 103 | { Quantity.mostly, "很多" }, 104 | { Quantity.always, "大量" }, 105 | { Quantity.insane, "疯狂" }, 106 | 107 | { OceanQuantity.ocean_never, "无" }, 108 | { OceanQuantity.ocean_rare, "很少" }, 109 | { OceanQuantity.ocean_uncommon, "较少" }, 110 | { OceanQuantity.ocean_default, "默认" }, 111 | { OceanQuantity.ocean_often, "较多" }, 112 | { OceanQuantity.ocean_mostly, "很多" }, 113 | { OceanQuantity.ocean_always, "大量" }, 114 | { OceanQuantity.ocean_insane, "疯狂" }, 115 | 116 | { PrefabswapsStart.classic, "经典" }, 117 | { PrefabswapsStart.@default, "默认" }, 118 | { PrefabswapsStart.highly_random, "非常随机" }, 119 | 120 | }).Value; 121 | 122 | private DstEnumText() { } 123 | 124 | public new string this[Enum e] 125 | { 126 | get 127 | { 128 | if (TryGetValue(e, out var value)) 129 | return value; 130 | return string.Empty; 131 | } 132 | } 133 | 134 | [return: NotNullIfNotNull(nameof(defaultValue))] 135 | public string? TryGetValueOrDefault(Enum e, string? defaultValue = null) 136 | { 137 | if (TryGetValue(e, out var value)) 138 | return value; 139 | return defaultValue; 140 | } 141 | 142 | 143 | private readonly Dictionary stringEnum = new(StringComparer.OrdinalIgnoreCase) 144 | { 145 | { "unknown", "未知" }, 146 | { "survival", "生存" }, 147 | { "wilderness", "荒野" }, 148 | { "endless", "无尽" }, 149 | { "lavaarena", "熔炉" }, 150 | { "quagmire", "暴食" }, 151 | { "starving_floor", "StarvingFloor" }, 152 | { "smashup", "Smashup" }, 153 | { "autumn", "秋" }, 154 | { "winter", "冬" }, 155 | { "spring", "春" }, 156 | { "summer", "夏" }, 157 | { "autumnOrspring", "春活秋" }, 158 | { "winterOrsummer", "冬季或夏季" }, 159 | { "autumnOrwinterOrspringOrsummer", "随机" }, 160 | 161 | { "relaxed", "轻松" }, 162 | { "lightsout", "暗无天日" }, 163 | { "cooperative", "合作" }, 164 | { "ooperative", "合作" }, 165 | { "social", "社交" }, 166 | { "madness", "疯狂" }, 167 | { "competitive", "竞争" }, 168 | { "oceanfishing", "海钓" }, 169 | }; 170 | 171 | public string GetFromString(string str, string @default) 172 | { 173 | if (stringEnum.TryGetValue(str, out var value)) 174 | { 175 | return value; 176 | } 177 | return @default; 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /DstServerQuery/Helpers/JsonSourceGeneraterContext.cs: -------------------------------------------------------------------------------- 1 | using DstServerQuery.Models; 2 | using DstServerQuery.Models.Lobby; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace DstServerQuery.Helpers; 6 | 7 | [JsonSerializable(typeof(bool))] 8 | [JsonSerializable(typeof(string))] 9 | [JsonSerializable(typeof(int))] 10 | [JsonSerializable(typeof(object))] 11 | [JsonSerializable(typeof(WorldLevelRawItem))] 12 | [JsonSourceGenerationOptions( 13 | NumberHandling = JsonNumberHandling.AllowReadingFromString, 14 | PropertyNamingPolicy = JsonKnownNamingPolicy.Unspecified, 15 | UseStringEnumConverter = true 16 | )] 17 | public partial class DstRawJsonContext : JsonSerializerContext; 18 | 19 | [JsonSerializable(typeof(LobbyPlayerInfo))] 20 | [JsonSerializable(typeof(LobbyDaysInfo))] 21 | [JsonSerializable(typeof(LobbyModInfo))] 22 | public partial class DstLobbyInfoJsonContext : JsonSerializerContext; -------------------------------------------------------------------------------- /DstServerQuery/Helpers/LuaTempEnvironment.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using System.Diagnostics; 3 | using MoonSharp.Interpreter; 4 | 5 | namespace DstServerQuery.Helpers; 6 | 7 | public class LuaTempEnvironment 8 | { 9 | public static LuaTempEnvironment Instance { get; set; } 10 | private readonly BlockingCollection