mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-25 23:18:56 -07:00
Radicade Public Code
This commit is contained in:
+1554
@@ -0,0 +1,1554 @@
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using SixLabors.ImageSharp.Web.DependencyInjection;
|
||||
using System.Diagnostics;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using RadicadeServer;
|
||||
using RadicadeServer.API.Notifications;
|
||||
using RadicadeServer.API.Relationships;
|
||||
using RadicadeServer.Admin;
|
||||
using RadicadeServer.Auth;
|
||||
using RadicadeServer.DB;
|
||||
using RadicadeServer.Extensions;
|
||||
using RadicadeServer.RecNet.Accounts.Local;
|
||||
using RadicadeServer.RecNet.Config;
|
||||
using RadicadeServer.RecNet.Rooms;
|
||||
using RadicadeServer.RecNet.Storefront;
|
||||
using RadicadeServer.RecNet.Enums;
|
||||
using RadicadeServer.RecNet.Enums.Rooms;
|
||||
using RadicadeServer.Radicade.Discord;
|
||||
using RadicadeServer.Discord.Webhooks;
|
||||
using RadicadeServer.Utils;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const string SplitTestHardOverrides = "";
|
||||
const string LocalHttpsCertPassword = "radicade-local-dev-cert";
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
DBConfig.LoadConfiguration(builder.Configuration);
|
||||
WebhookConfig.Load(builder.Configuration, builder.Environment.ContentRootPath);
|
||||
|
||||
|
||||
builder.Logging.ClearProviders();
|
||||
builder.Logging.AddSimpleConsole(options =>
|
||||
{
|
||||
options.SingleLine = true;
|
||||
options.TimestampFormat = "yyyy-MM-dd HH:mm:ss ";
|
||||
});
|
||||
builder.Logging.AddFilter("Microsoft.EntityFrameworkCore.Database.Command", LogLevel.Warning);
|
||||
builder.Logging.AddFilter("Microsoft.EntityFrameworkCore.Infrastructure", LogLevel.Warning);
|
||||
builder.Logging.AddFilter("Microsoft.EntityFrameworkCore.Query", LogLevel.Error);
|
||||
var listenUrls = builder.Configuration["Radicade:ListenUrls"];
|
||||
if (!string.IsNullOrWhiteSpace(listenUrls))
|
||||
{
|
||||
builder.WebHost.UseUrls(listenUrls.Split(';', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries));
|
||||
}
|
||||
else if (builder.Environment.IsProduction())
|
||||
{
|
||||
builder.WebHost.UseUrls("http://0.0.0.0:3030");
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.WebHost.UseUrls("https://localhost:3030");
|
||||
}
|
||||
|
||||
if (builder.Environment.IsProduction())
|
||||
{
|
||||
var missingSecrets = new List<string>();
|
||||
if (string.IsNullOrWhiteSpace(DBConfig.AdminApiKey)) missingSecrets.Add("Radicade:AdminApiKey");
|
||||
if (string.IsNullOrWhiteSpace(DBConfig.JwtKey)) missingSecrets.Add("Radicade:JwtKey");
|
||||
if (string.IsNullOrWhiteSpace(DBConfig.SteamApiKey)) missingSecrets.Add("Radicade:SteamApiKey");
|
||||
|
||||
if (missingSecrets.Count > 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Missing required production secrets: " + string.Join(", ", missingSecrets) +
|
||||
". Set these as environment variables (e.g. Radicade__SteamApiKey) or in a secrets manager " +
|
||||
"— never commit real values into appsettings.Production.json.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(DBConfig.JwtKey))
|
||||
{
|
||||
DBConfig.JwtKey = "radicade-local-development-jwt-key-change-before-production";
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(DBConfig.AdminApiKey))
|
||||
{
|
||||
DBConfig.AdminApiKey = "radicade-local-development-admin-key";
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(DBConfig.SteamApiKey) &&
|
||||
string.IsNullOrWhiteSpace(builder.Configuration["Radicade:TrustUnverifiedSteamIdentity"]))
|
||||
{
|
||||
DBConfig.TrustUnverifiedSteamIdentity = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!Directory.Exists(DBConfig.CDNImages))
|
||||
Directory.CreateDirectory(DBConfig.CDNImages);
|
||||
if (!Directory.Exists(DBConfig.CDNStorage))
|
||||
Directory.CreateDirectory(DBConfig.CDNStorage);
|
||||
if (!Directory.Exists(DBConfig.CDNVideos))
|
||||
Directory.CreateDirectory(DBConfig.CDNVideos);
|
||||
if (!Directory.Exists(DBConfig.CDNCustom))
|
||||
Directory.CreateDirectory(DBConfig.CDNCustom);
|
||||
if (!Directory.Exists(DBConfig.PersistentLogs))
|
||||
Directory.CreateDirectory(DBConfig.PersistentLogs);
|
||||
var dataProtectionKeys = Path.Join(DBConfig.PublicData, "Persistent", "DataProtection-Keys");
|
||||
if (!Directory.Exists(dataProtectionKeys))
|
||||
Directory.CreateDirectory(dataProtectionKeys);
|
||||
var localHttpsCertPath = Path.Join(DBConfig.PublicData, "Persistent", "localhost-rsa-aes-devcert.pfx");
|
||||
if (builder.Environment.IsDevelopment())
|
||||
{
|
||||
EnsureLocalHttpsCertificate(localHttpsCertPath, LocalHttpsCertPassword);
|
||||
var localHttpsCertificate = LoadLocalHttpsCertificateFromStore(
|
||||
InstallLocalHttpsCertificate(localHttpsCertPath, LocalHttpsCertPassword));
|
||||
builder.WebHost.ConfigureKestrel(options =>
|
||||
{
|
||||
options.ConfigureHttpsDefaults(httpsOptions =>
|
||||
{
|
||||
httpsOptions.ServerCertificate = localHttpsCertificate;
|
||||
httpsOptions.SslProtocols = System.Security.Authentication.SslProtocols.Tls12;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
DBConfig.DebugKey = Guid.NewGuid().AltGuid() + Guid.NewGuid().AltGuid();
|
||||
File.WriteAllText(Path.Join(DBConfig.PersistentLogs, "debugkey.txt"), DBConfig.DebugKey);
|
||||
|
||||
DBConfig.LoadFiles();
|
||||
|
||||
|
||||
|
||||
|
||||
SteamWhitelist.Load();
|
||||
|
||||
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
|
||||
if (string.IsNullOrWhiteSpace(connectionString))
|
||||
throw new InvalidOperationException("Missing ConnectionStrings:DefaultConnection. Set ConnectionStrings__DefaultConnection on the VPS or add it to local appsettings.Development.json.");
|
||||
|
||||
builder.Services.AddDbContextFactory<RecDBContext>(
|
||||
options => options.UseSqlServer(connectionString, sql => { sql.EnableRetryOnFailure(3); })
|
||||
.ConfigureWarnings(w => w.Ignore(RelationalEventId.PendingModelChangesWarning))
|
||||
);
|
||||
|
||||
var key = Encoding.ASCII.GetBytes(DBConfig.JwtKey);
|
||||
builder.Services.AddAuthentication(options =>
|
||||
{
|
||||
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
})
|
||||
.AddJwtBearer(options =>
|
||||
{
|
||||
options.RequireHttpsMetadata = false;
|
||||
options.SaveToken = true;
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuerSigningKey = true,
|
||||
IssuerSigningKey = new SymmetricSecurityKey(key),
|
||||
ValidateIssuer = false,
|
||||
ValidateAudience = false,
|
||||
};
|
||||
options.Events = new JwtBearerEvents
|
||||
{
|
||||
OnMessageReceived = context =>
|
||||
{
|
||||
var accessToken = context.Request.Query["access_token"];
|
||||
var path = context.HttpContext.Request.Path;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(accessToken) &&
|
||||
RadicadeServer.API.Notifications.NotifyHubRoutes.IsHubPath(path.Value))
|
||||
{
|
||||
context.Token = accessToken;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
OnTokenValidated = async context =>
|
||||
{
|
||||
var principal = context.Principal;
|
||||
if (principal == null) return;
|
||||
|
||||
var userIdClaim = principal.FindFirst("userId");
|
||||
var versionClaim = principal.FindFirst("tver");
|
||||
if (userIdClaim == null || versionClaim == null) return;
|
||||
|
||||
if (long.TryParse(userIdClaim.Value, out var userId) &&
|
||||
int.TryParse(versionClaim.Value, out var tokenVersion) &&
|
||||
!await TokenRevocation.IsValidAsync(context.HttpContext.RequestServices, userId, tokenVersion))
|
||||
{
|
||||
context.Fail("Session revoked. Please sign in again.");
|
||||
}
|
||||
},
|
||||
OnAuthenticationFailed = context =>
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
OnChallenge = context =>
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
builder.Services.AddImageSharp(options =>
|
||||
{
|
||||
options.OnParseCommandsAsync = context =>
|
||||
{
|
||||
var commands = context.Commands;
|
||||
|
||||
if (commands.TryGetValue("cropSquare", out var cropValue) &&
|
||||
cropValue == "1" &&
|
||||
commands.TryGetValue("width", out var widthValue) &&
|
||||
int.TryParse(widthValue, out int width))
|
||||
{
|
||||
commands["height"] = width.ToString();
|
||||
commands["rmode"] = "crop";
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
});
|
||||
builder.Services.AddDataProtection()
|
||||
.PersistKeysToFileSystem(new DirectoryInfo(dataProtectionKeys));
|
||||
builder.Services.AddControllers().AddJsonOptions(options =>
|
||||
{
|
||||
options.JsonSerializerOptions.PropertyNamingPolicy = null;
|
||||
options.JsonSerializerOptions.WriteIndented = true;
|
||||
});
|
||||
builder.Services.Configure<FormOptions>(options =>
|
||||
{
|
||||
options.MultipartBodyLengthLimit = 256L * 1024 * 1024;
|
||||
});
|
||||
builder.Services.AddSignalR(options => Microsoft.AspNetCore.SignalR.HubOptionsExtensions
|
||||
.AddFilter<RadicadeServer.API.Notifications.NotifyHubInvocationFilter>(options));
|
||||
builder.Services.AddSingleton<RadicadeServer.API.Notifications.NotifyHubInvocationFilter>();
|
||||
builder.Services.AddHttpClient();
|
||||
|
||||
|
||||
|
||||
|
||||
builder.Services.AddHostedService<BanStatusMonitor>();
|
||||
|
||||
|
||||
var isDevelopment = builder.Environment.IsDevelopment();
|
||||
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy("AllowAllOrigins", builder =>
|
||||
builder.AllowAnyOrigin()
|
||||
.AllowAnyMethod()
|
||||
.AllowAnyHeader());
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
options.AddPolicy("AdminPanel", builder =>
|
||||
{
|
||||
var adminOrigins = new List<string>();
|
||||
if (!string.IsNullOrWhiteSpace(DBConfig.AdminPanelUrl))
|
||||
adminOrigins.Add(DBConfig.AdminPanelUrl);
|
||||
|
||||
if (adminOrigins.Count > 0)
|
||||
{
|
||||
builder.WithOrigins(adminOrigins.ToArray())
|
||||
.AllowAnyMethod()
|
||||
.AllowAnyHeader()
|
||||
.AllowCredentials();
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.WithOrigins("http://localhost:0")
|
||||
.AllowAnyMethod()
|
||||
.AllowAnyHeader()
|
||||
.AllowCredentials();
|
||||
}
|
||||
|
||||
if (isDevelopment)
|
||||
{
|
||||
builder.SetIsOriginAllowed(origin =>
|
||||
{
|
||||
if (!Uri.TryCreate(origin, UriKind.Absolute, out var uri))
|
||||
return false;
|
||||
return uri.Host is "localhost" or "127.0.0.1" or "[::1]";
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<RecDBContext>();
|
||||
dbContext.Database.Migrate();
|
||||
await EnsureSubRoomUnityAssetColumn(dbContext);
|
||||
await EnsureRoomJoinGiftColumns(dbContext);
|
||||
await EnsureRoomDeveloperOwnedColumn(dbContext);
|
||||
await EnsurePlayerEventColumns(dbContext);
|
||||
await EnsureVoiceChatRestrictionTable(dbContext);
|
||||
await EnsureValidSceneLocations(dbContext);
|
||||
await EnsureCatalogTables(dbContext);
|
||||
StageShowdownRoomContent();
|
||||
await EnsureProfileIdentityColumns(dbContext);
|
||||
if (DBConfig.TestInfluencerAccountIds.Count > 0)
|
||||
{
|
||||
var testProfiles = await dbContext.Profiles
|
||||
.Where(profile => DBConfig.TestInfluencerAccountIds.Contains(profile.accountId))
|
||||
.ToListAsync();
|
||||
foreach (var profile in testProfiles)
|
||||
{
|
||||
if (!profile.HasRole("influencer"))
|
||||
profile.role = string.IsNullOrWhiteSpace(profile.role) ? "influencer" : $"{profile.role},influencer";
|
||||
}
|
||||
if (testProfiles.Any(profile => profile.HasRole("influencer")))
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
await RemoveBlanketCoOwners(dbContext);
|
||||
|
||||
await SyncAdminAccounts(dbContext);
|
||||
|
||||
if (!dbContext.Profiles.Any())
|
||||
{
|
||||
await dbContext.AddCoach();
|
||||
await dbContext.AddGameConfigs();
|
||||
await dbContext.AddServerConfig();
|
||||
await dbContext.AddConfig();
|
||||
await dbContext.AddDefaultRooms();
|
||||
}
|
||||
|
||||
if (!await dbContext.Rooms.AnyAsync(room => room.IsDorm))
|
||||
{
|
||||
await dbContext.AddDormRoom();
|
||||
}
|
||||
|
||||
await dbContext.NormalizeDormRoomImagesAsync();
|
||||
|
||||
await DefaultRooms.EnsureAsync(dbContext);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
await FirstTimeUtils.DownloadImages();
|
||||
|
||||
LoadingScreenImages.EnsureInstalled();
|
||||
|
||||
await EnsureRequiredGameConfigs(dbContext);
|
||||
await RepairAvatarBootData(dbContext);
|
||||
await EnsureReferenceGameConfigs(dbContext);
|
||||
await RepairSplitTestConfigs(dbContext);
|
||||
await DisableCommunityBoardHowToBuildLink(dbContext);
|
||||
await RepairPlayerSettingsBootData(dbContext);
|
||||
await RepairNullProfileImages(dbContext);
|
||||
|
||||
StorefrontCatalog.Load();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if (SteamWhitelist.CreatedEmptyConfig)
|
||||
{
|
||||
var wlOnly = await dbContext.ServerConfig.AsNoTracking().FirstOrDefaultAsync(x => x.Key == "WhitelistOnly");
|
||||
var wl = await dbContext.ServerConfig.AsNoTracking().FirstOrDefaultAsync(x => x.Key == "SteamWhitelist");
|
||||
|
||||
if (wl != null && !string.IsNullOrWhiteSpace(wl.Value))
|
||||
{
|
||||
SteamWhitelist.SetSteamIds(wl.Value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries));
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (wlOnly != null && wlOnly.Value.Equals("true", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
SteamWhitelist.Enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
await MatchmakingRuntimeState.ResetAsync(dbContext);
|
||||
|
||||
MainConfig.ActiveConfig = await dbContext.Config.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
static void StageShowdownRoomContent()
|
||||
{
|
||||
const string saveName = "Showdown.room";
|
||||
const string bundleName = "Showdown_0_1.assetbundle";
|
||||
var candidates = new[]
|
||||
{
|
||||
Path.GetFullPath(Path.Combine("..", "RoomImports", "Showdown")),
|
||||
Path.GetFullPath(Path.Combine("..", "room-imports", "Showdown")),
|
||||
Path.GetFullPath(Path.Combine("RoomImports", "Showdown")),
|
||||
Path.GetFullPath(Path.Combine("room-imports", "Showdown")),
|
||||
};
|
||||
var sourceDir = candidates.FirstOrDefault(Directory.Exists) ?? candidates[0];
|
||||
var storage = DBConfig.CDNStorage;
|
||||
Directory.CreateDirectory(storage);
|
||||
var saveSource = Path.Combine(sourceDir, saveName);
|
||||
var bundleSource = Path.Combine(sourceDir, bundleName);
|
||||
if (!File.Exists(saveSource) || !File.Exists(bundleSource))
|
||||
{
|
||||
return;
|
||||
}
|
||||
File.Copy(saveSource, Path.Combine(storage, saveName), overwrite: true);
|
||||
File.Copy(bundleSource, Path.Combine(storage, bundleName), overwrite: true);
|
||||
}
|
||||
|
||||
static async Task SyncAdminAccounts(RecDBContext dbContext)
|
||||
{
|
||||
var configured = DBConfig.AdminAccounts;
|
||||
var existing = await dbContext.AdminUsers.ToListAsync();
|
||||
|
||||
if (configured.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var account in configured)
|
||||
{
|
||||
var user = existing.FirstOrDefault(x => string.Equals(x.Username, account.Username, StringComparison.OrdinalIgnoreCase));
|
||||
if (user == null)
|
||||
{
|
||||
dbContext.AdminUsers.Add(new AdminUser
|
||||
{
|
||||
Username = account.Username,
|
||||
PasswordHash = AdminSecurity.HashPassword(account.Password),
|
||||
CreatedAt = DateTime.UtcNow
|
||||
});
|
||||
await dbContext.SaveChangesAsync();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!AdminSecurity.VerifyPassword(user.PasswordHash, account.Password))
|
||||
{
|
||||
user.PasswordHash = AdminSecurity.HashPassword(account.Password);
|
||||
await dbContext.SaveChangesAsync();
|
||||
await AdminSession.RevokeAllAsync(dbContext, user.Id);
|
||||
}
|
||||
else if (AdminSecurity.NeedsRehash(user.PasswordHash))
|
||||
{
|
||||
user.PasswordHash = AdminSecurity.HashPassword(account.Password);
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
var removed = existing
|
||||
.Where(user => !configured.Any(account => string.Equals(account.Username, user.Username, StringComparison.OrdinalIgnoreCase)))
|
||||
.ToList();
|
||||
if (removed.Count == 0) return;
|
||||
|
||||
foreach (var user in removed)
|
||||
{
|
||||
await AdminSession.RevokeAllAsync(dbContext, user.Id);
|
||||
}
|
||||
dbContext.AdminUsers.RemoveRange(removed);
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
static async Task RemoveBlanketCoOwners(RecDBContext dbContext)
|
||||
{
|
||||
const string marker = "Config.BlanketCoOwnersRemoved";
|
||||
if (await dbContext.GameConfig.AnyAsync(x => x.Key == marker)) return;
|
||||
|
||||
var accounts = new List<long> { 2 };
|
||||
accounts.AddRange(DBConfig.TestCoOwnerAccountIds);
|
||||
accounts = accounts.Distinct().ToList();
|
||||
|
||||
var rows = await dbContext.RoomRoles
|
||||
.Where(role => accounts.Contains(role.AccountId) && role.Role == RoleType.CoOwner)
|
||||
.ToListAsync();
|
||||
|
||||
if (rows.Count > 0)
|
||||
{
|
||||
dbContext.RoomRoles.RemoveRange(rows);
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
dbContext.GameConfig.Add(new ConfigVal(marker, "1"));
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
static async Task EnsureProfileIdentityColumns(RecDBContext dbContext)
|
||||
{
|
||||
await dbContext.Database.ExecuteSqlRawAsync(@"
|
||||
IF COL_LENGTH('dbo.Profiles', 'personalPronouns') IS NULL
|
||||
ALTER TABLE [Profiles] ADD [personalPronouns] int NOT NULL CONSTRAINT [DF_Profiles_personalPronouns] DEFAULT 0;
|
||||
IF COL_LENGTH('dbo.Profiles', 'identityFlags') IS NULL
|
||||
ALTER TABLE [Profiles] ADD [identityFlags] int NOT NULL CONSTRAINT [DF_Profiles_identityFlags] DEFAULT 0;
|
||||
-- Profile.bannerImage / isRecentHistoryVisible. Added by a migration too, but a
|
||||
-- production database that drifted (see EnsurePlayerEventColumns) would reach the
|
||||
-- model before the column exists, and every Profiles query would then fail with an
|
||||
-- invalid-column-name error. Idempotent, so the migration and this repair can coexist.
|
||||
IF COL_LENGTH('dbo.Profiles', 'bannerImage') IS NULL
|
||||
ALTER TABLE [Profiles] ADD [bannerImage] nvarchar(max) NULL;
|
||||
IF COL_LENGTH('dbo.Profiles', 'isRecentHistoryVisible') IS NULL
|
||||
ALTER TABLE [Profiles] ADD [isRecentHistoryVisible] bit NOT NULL CONSTRAINT [DF_Profiles_isRecentHistoryVisible] DEFAULT 1;");
|
||||
}
|
||||
|
||||
static async Task EnsureSubRoomUnityAssetColumn(RecDBContext dbContext)
|
||||
{
|
||||
await dbContext.Database.ExecuteSqlRawAsync(@"
|
||||
IF COL_LENGTH('dbo.SubRooms', 'UnityAssetId') IS NULL
|
||||
ALTER TABLE [SubRooms] ADD [UnityAssetId] nvarchar(max) NOT NULL CONSTRAINT [DF_SubRooms_UnityAssetId] DEFAULT N'';");
|
||||
}
|
||||
|
||||
static async Task RepairNullProfileImages(RecDBContext dbContext)
|
||||
{
|
||||
var broken = await dbContext.Profiles
|
||||
.Where(p => p.profileImage == null || p.profileImage == "")
|
||||
.ToListAsync();
|
||||
if (broken.Count == 0)
|
||||
return;
|
||||
|
||||
foreach (var profile in broken)
|
||||
profile.profileImage = "DefaultProfileImage";
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
RoomUtils.DbFactory = app.Services.GetRequiredService<IDbContextFactory<RecDBContext>>();
|
||||
MatchmakingUtils.DbFactory = app.Services.GetRequiredService<IDbContextFactory<RecDBContext>>();
|
||||
Notify.DbFactory = app.Services.GetRequiredService<IDbContextFactory<RecDBContext>>();
|
||||
Notify.Context = app.Services.GetRequiredService<Microsoft.AspNetCore.SignalR.IHubContext<NotifyHub>>();
|
||||
RelationshipsShared.DbFactory = app.Services.GetRequiredService<IDbContextFactory<RecDBContext>>();
|
||||
PlayerUtils.DbFactory = app.Services.GetRequiredService<IDbContextFactory<RecDBContext>>();
|
||||
AuthShared.DbFactory = app.Services.GetRequiredService<IDbContextFactory<RecDBContext>>();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
app.Use(async (context, next) =>
|
||||
{
|
||||
var path = context.Request.Path.Value ?? "";
|
||||
if (!path.StartsWith("/admin/", StringComparison.OrdinalIgnoreCase) &&
|
||||
!string.Equals(path, "/admin", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var ip = context.Request.GetClientIp();
|
||||
if (!string.IsNullOrWhiteSpace(ip))
|
||||
{
|
||||
var dbFactory = context.RequestServices.GetRequiredService<IDbContextFactory<RecDBContext>>();
|
||||
if (await GameBlockMonitor.IsBlockedAsync(dbFactory, ip))
|
||||
{
|
||||
context.Response.StatusCode = StatusCodes.Status403Forbidden;
|
||||
await context.Response.WriteAsJsonAsync(new
|
||||
{
|
||||
error = "game_blocked",
|
||||
message = "This IP address is blocked from connecting to the game servers."
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await next();
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
app.Use(async (context, next) =>
|
||||
{
|
||||
var path = context.Request.Path.Value ?? "";
|
||||
if (path.Contains("//"))
|
||||
{
|
||||
var collapsed = System.Text.RegularExpressions.Regex.Replace(path, "/{2,}", "/");
|
||||
context.Request.Path = collapsed;
|
||||
}
|
||||
|
||||
await next();
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
app.UseRouting();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
app.UseCors("AllowAllOrigins");
|
||||
|
||||
|
||||
app.Use(async (context, next) =>
|
||||
{
|
||||
var path = context.Request.Path.Value ?? "";
|
||||
if (path.StartsWith("/images/", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
context.Response.Headers.Append("Access-Control-Allow-Origin", "*");
|
||||
context.Response.Headers.Append("Access-Control-Allow-Methods", "GET, OPTIONS");
|
||||
context.Response.Headers.Append("Access-Control-Allow-Headers", "*");
|
||||
}
|
||||
|
||||
await next();
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
app.Use(async (context, next) =>
|
||||
{
|
||||
var path = context.Request.Path.Value ?? "";
|
||||
if (path.StartsWith("/admin/", StringComparison.OrdinalIgnoreCase) &&
|
||||
!path.StartsWith("/admin/auth", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var existingKey = context.Request.Query["key"].ToString();
|
||||
if (string.IsNullOrEmpty(existingKey) || !existingKey.IsAdminKey())
|
||||
{
|
||||
void Promote()
|
||||
{
|
||||
var query = new Dictionary<string, Microsoft.Extensions.Primitives.StringValues>();
|
||||
foreach (var kv in context.Request.Query)
|
||||
query[kv.Key] = kv.Value;
|
||||
query["key"] = DBConfig.AdminApiKey;
|
||||
context.Request.Query = new QueryCollection(query);
|
||||
}
|
||||
|
||||
if (context.Request.Cookies.TryGetValue(RadicadeServer.Auth.AdminSession.CookieName, out var sessionToken))
|
||||
{
|
||||
var dbFactory = context.RequestServices.GetRequiredService<IDbContextFactory<RecDBContext>>();
|
||||
await using var authDb = await dbFactory.CreateDbContextAsync();
|
||||
var (valid, _) = await RadicadeServer.Auth.AdminSession.ValidateAsync(authDb, sessionToken);
|
||||
if (valid) Promote();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await next();
|
||||
});
|
||||
|
||||
app.Use(async (context, next) =>
|
||||
{
|
||||
var started = Stopwatch.GetTimestamp();
|
||||
|
||||
var originalBody = context.Response.Body;
|
||||
var countingBody = new CountingStream(originalBody);
|
||||
context.Response.Body = countingBody;
|
||||
try
|
||||
{
|
||||
await next();
|
||||
}
|
||||
catch (OperationCanceledException) when (context.RequestAborted.IsCancellationRequested)
|
||||
{
|
||||
Console.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] {context.Request.Method} {context.Request.Path}{context.Request.QueryString} -> client disconnected mid-response (len={countingBody.BytesWritten})");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] unhandled {context.Request.Method} {context.Request.Path}{context.Request.QueryString}: {ex}");
|
||||
if (!context.Response.HasStarted && !context.RequestAborted.IsCancellationRequested)
|
||||
{
|
||||
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
|
||||
await context.Response.WriteAsJsonAsync(new { error = "internal_server_error" });
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
var elapsed = Stopwatch.GetElapsedTime(started).TotalMilliseconds;
|
||||
var bodyPreview = countingBody.BytesWritten <= 8
|
||||
? " body=" + System.Text.Encoding.UTF8.GetString(countingBody.Peek)
|
||||
: string.Empty;
|
||||
Console.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] {context.Request.Method} {context.Request.Path}{context.Request.QueryString} -> {context.Response.StatusCode} ({elapsed:0.0} ms) len={countingBody.BytesWritten} ct={context.Response.ContentType}{bodyPreview}");
|
||||
}
|
||||
});
|
||||
|
||||
app.UseMiddleware<RadicadeServer.Middleware.FailureEnvelopeMiddleware>();
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.Use(async (context, next) =>
|
||||
{
|
||||
var diagnosticPaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"/matchmaking/player",
|
||||
"/matchmaking/player/login",
|
||||
"/matchmaking/player/exclusivelogin",
|
||||
"/leaderboard/GetRanks",
|
||||
"/leaderboard/GetPlayerRank",
|
||||
"/leaderboard/GetNearbyScores",
|
||||
"/leaderboard/leaderboard/GetRanks",
|
||||
"/leaderboard/leaderboard/GetPlayerRank",
|
||||
"/leaderboard/leaderboard/GetNearbyScores",
|
||||
"/econ/api/checklist/v1/current",
|
||||
"/econ/api/objectives/v1/myprogress",
|
||||
"/api/players/v2/progression/bulk",
|
||||
"/accounts/account/bulk",
|
||||
"/api/playerReputation/v2/bulk",
|
||||
"/api/relationships/v2/get",
|
||||
"/api/PlayerReporting/v1/moderationBlockDetails",
|
||||
"/chat/thread",
|
||||
"/playersettings",
|
||||
"/playersettings/v2",
|
||||
"/playersettings/playersettings",
|
||||
"/api/settings",
|
||||
"/api/settings/v2",
|
||||
"/api/settings/playersettings",
|
||||
"/api/gameconfigs/v1/all",
|
||||
"/api/versioncheck/v4"
|
||||
};
|
||||
|
||||
var path = context.Request.Path.Value ?? string.Empty;
|
||||
if (!diagnosticPaths.Contains(path))
|
||||
{
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
|
||||
var originalBody = context.Response.Body;
|
||||
await using var capturedBody = new MemoryStream();
|
||||
context.Response.Body = capturedBody;
|
||||
|
||||
try
|
||||
{
|
||||
await next();
|
||||
|
||||
capturedBody.Position = 0;
|
||||
using var reader = new StreamReader(
|
||||
capturedBody,
|
||||
Encoding.UTF8,
|
||||
detectEncodingFromByteOrderMarks: false,
|
||||
leaveOpen: true);
|
||||
|
||||
var responseBody = await reader.ReadToEndAsync();
|
||||
var diagnosticLine =
|
||||
$"[PlayerDataDebug] {context.Request.Method} " +
|
||||
$"{context.Request.Path}{context.Request.QueryString} -> " +
|
||||
$"{context.Response.StatusCode} body={responseBody}";
|
||||
try
|
||||
{
|
||||
var diagnosticLogPath = Path.Join(DBConfig.PersistentLogs, "player-data-debug.log");
|
||||
await File.AppendAllTextAsync(
|
||||
diagnosticLogPath,
|
||||
$"[{DateTime.UtcNow:O}] {diagnosticLine}{Environment.NewLine}");
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
capturedBody.Position = 0;
|
||||
await capturedBody.CopyToAsync(originalBody);
|
||||
}
|
||||
finally
|
||||
{
|
||||
context.Response.Body = originalBody;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
app.Use(async (context, next) =>
|
||||
{
|
||||
var leaderboardPath = context.Request.Path.Value ?? string.Empty;
|
||||
if (leaderboardPath.Contains("leaderboard", StringComparison.OrdinalIgnoreCase)
|
||||
|| leaderboardPath.Contains("scoreboard", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
context.Request.EnableBuffering();
|
||||
string requestBody;
|
||||
using (var reader = new StreamReader(
|
||||
context.Request.Body,
|
||||
Encoding.UTF8,
|
||||
detectEncodingFromByteOrderMarks: false,
|
||||
leaveOpen: true))
|
||||
{
|
||||
requestBody = await reader.ReadToEndAsync();
|
||||
}
|
||||
context.Request.Body.Position = 0;
|
||||
}
|
||||
|
||||
await next();
|
||||
});
|
||||
|
||||
|
||||
app.MapControllers();
|
||||
foreach (var hubPath in RadicadeServer.API.Notifications.NotifyHubRoutes.Paths)
|
||||
app.MapHub<NotifyHub>(hubPath);
|
||||
|
||||
app.MapGet("/diag/build", () => Results.Json(new
|
||||
{
|
||||
build = BuildInfo.Tag,
|
||||
notifyIdFormat = BuildInfo.NotifyIdFormat,
|
||||
assemblyBuiltUtc = BuildInfo.AssemblyBuiltUtc,
|
||||
startedUtc = BuildInfo.StartedUtc,
|
||||
}));
|
||||
|
||||
app.MapGet("/diag/notify", async (long account, string? key, string? form) =>
|
||||
{
|
||||
if (key is null || !key.IsAdminKey()) return Results.Unauthorized();
|
||||
return Results.Text(await Notify.ReplayStoredCoachMessageAsync(account, form), "text/plain");
|
||||
});
|
||||
|
||||
app.MapGet("/diag/rewardpush", async (long account, string? key, string? id, IDbContextFactory<RecDBContext> dbFactory) =>
|
||||
{
|
||||
if (key is null || !key.IsAdminKey()) return Results.Unauthorized();
|
||||
|
||||
await using var db = await dbFactory.CreateDbContextAsync();
|
||||
return Results.Text(await RewardPushDiag.ReplayPendingRewardSelectionAsync(db, account, id), "text/plain");
|
||||
});
|
||||
|
||||
app.MapGet("/diag/rewards", async (long account, string? key, int? lines, IDbContextFactory<RecDBContext> dbFactory) =>
|
||||
{
|
||||
if (key is null || !key.IsAdminKey()) return Results.Unauthorized();
|
||||
|
||||
await using var db = await dbFactory.CreateDbContextAsync();
|
||||
return Results.Text(await RewardDiag.DescribeAsync(db, account, lines ?? 120), "text/plain");
|
||||
});
|
||||
|
||||
app.Lifetime.ApplicationStarted.Register(() =>
|
||||
Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] [Radicade] started. env={app.Environment.EnvironmentName} url={DBConfig.ServerUrl} build={BuildInfo.Tag} assemblyBuilt={BuildInfo.AssemblyBuiltUtc:u}"));
|
||||
|
||||
app.Lifetime.ApplicationStopping.Register(() =>
|
||||
Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] [Radicade] stopping."));
|
||||
|
||||
app.Run();
|
||||
|
||||
static void EnsureLocalHttpsCertificate(string certificatePath, string password)
|
||||
{
|
||||
if (File.Exists(certificatePath)) return;
|
||||
|
||||
var certificateDirectory = Path.GetDirectoryName(certificatePath);
|
||||
if (!string.IsNullOrWhiteSpace(certificateDirectory) && !Directory.Exists(certificateDirectory))
|
||||
Directory.CreateDirectory(certificateDirectory);
|
||||
|
||||
using var rsa = CreateLocalHttpsKey();
|
||||
var request = new CertificateRequest(
|
||||
"CN=localhost",
|
||||
rsa,
|
||||
HashAlgorithmName.SHA256,
|
||||
RSASignaturePadding.Pkcs1);
|
||||
|
||||
request.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false, 0, false));
|
||||
request.CertificateExtensions.Add(new X509KeyUsageExtension(
|
||||
X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.KeyEncipherment,
|
||||
false));
|
||||
var enhancedKeyUsages = new OidCollection
|
||||
{
|
||||
new Oid("1.3.6.1.5.5.7.3.1")
|
||||
};
|
||||
request.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(enhancedKeyUsages, false));
|
||||
request.CertificateExtensions.Add(new X509SubjectKeyIdentifierExtension(request.PublicKey, false));
|
||||
|
||||
var subjectAltNames = new SubjectAlternativeNameBuilder();
|
||||
subjectAltNames.AddDnsName("localhost");
|
||||
subjectAltNames.AddIpAddress(IPAddress.Loopback);
|
||||
subjectAltNames.AddIpAddress(IPAddress.IPv6Loopback);
|
||||
request.CertificateExtensions.Add(subjectAltNames.Build());
|
||||
|
||||
using var certificate = request.CreateSelfSigned(
|
||||
DateTimeOffset.UtcNow.AddDays(-1),
|
||||
DateTimeOffset.UtcNow.AddYears(5));
|
||||
File.WriteAllBytes(certificatePath, certificate.Export(X509ContentType.Pfx, password));
|
||||
}
|
||||
|
||||
static X509Certificate2 LoadLocalHttpsCertificate(string certificatePath, string password)
|
||||
{
|
||||
return X509CertificateLoader.LoadPkcs12FromFile(
|
||||
certificatePath,
|
||||
password,
|
||||
X509KeyStorageFlags.MachineKeySet |
|
||||
X509KeyStorageFlags.PersistKeySet |
|
||||
X509KeyStorageFlags.Exportable);
|
||||
}
|
||||
|
||||
static string InstallLocalHttpsCertificate(string certificatePath, string password)
|
||||
{
|
||||
using var certificate = LoadLocalHttpsCertificate(certificatePath, password);
|
||||
using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
|
||||
store.Open(OpenFlags.ReadWrite);
|
||||
|
||||
var existingCertificates = store.Certificates.Find(
|
||||
X509FindType.FindByThumbprint,
|
||||
certificate.Thumbprint,
|
||||
validOnly: false);
|
||||
if (existingCertificates.Count == 0)
|
||||
{
|
||||
store.Add(certificate);
|
||||
}
|
||||
|
||||
return certificate.Thumbprint;
|
||||
}
|
||||
|
||||
static X509Certificate2 LoadLocalHttpsCertificateFromStore(string thumbprint)
|
||||
{
|
||||
using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
|
||||
store.Open(OpenFlags.ReadOnly);
|
||||
|
||||
var certificates = store.Certificates.Find(
|
||||
X509FindType.FindByThumbprint,
|
||||
thumbprint,
|
||||
validOnly: false);
|
||||
if (certificates.Count == 0)
|
||||
throw new InvalidOperationException($"Local HTTPS certificate {thumbprint} was not found in CurrentUser\\My.");
|
||||
|
||||
return certificates
|
||||
.OfType<X509Certificate2>()
|
||||
.OrderByDescending(certificate => certificate.NotAfter)
|
||||
.First();
|
||||
}
|
||||
|
||||
static RSA CreateLocalHttpsKey()
|
||||
{
|
||||
if (!OperatingSystem.IsWindows())
|
||||
return RSA.Create(2048);
|
||||
|
||||
return new RSACryptoServiceProvider(
|
||||
2048,
|
||||
new CspParameters(24, "Microsoft Enhanced RSA and AES Cryptographic Provider")
|
||||
{
|
||||
KeyContainerName = $"RadicadeLocalHttps-{Guid.NewGuid():N}",
|
||||
KeyNumber = (int)KeyNumber.Exchange
|
||||
})
|
||||
{
|
||||
PersistKeyInCsp = false
|
||||
};
|
||||
}
|
||||
|
||||
static async Task EnsureRequiredGameConfigs(RecDBContext dbContext)
|
||||
{
|
||||
await UpsertGameConfigValue(dbContext, "splitTestSoftOverrides", string.Empty);
|
||||
await UpsertGameConfigValue(dbContext, "splitTestsAvailable", SplitTestConfigs.TestsAvailable);
|
||||
await UpsertGameConfigValue(dbContext, "splitTestSegmentProbabilities", SplitTestConfigs.SegmentProbabilities);
|
||||
await UpsertGameConfigValue(dbContext, "splitTestHardOverrides", SplitTestConfigs.HardOverrides);
|
||||
var probabilitiesRow = await dbContext.GameConfig.FirstOrDefaultAsync(x => x.Key == "splitTestSegmentProbabilities");
|
||||
if (probabilitiesRow != null && probabilitiesRow.Value == "{}")
|
||||
probabilitiesRow.Value = SplitTestConfigs.SegmentProbabilities;
|
||||
await EnsureSplitTestHardOverrides(dbContext);
|
||||
await UpsertGameConfigValue(dbContext, "Backtrace.versionRegex", ".*");
|
||||
await UpsertGameConfigValue(dbContext, "Backtrace.messageRegex", ".*");
|
||||
await UpsertGameConfigValue(dbContext, "Backtrace.stopTimeUTC", "2099-01-01 00:00");
|
||||
|
||||
await UpsertGameConfigValue(dbContext, "Growth.FriendFestPromosActive", "false");
|
||||
await UpsertGameConfigValue(dbContext, "Growth.FriendFestImageName", "FriendFest.jpg");
|
||||
await UpsertGameConfigValue(dbContext, "Growth.EnableProfilePhoneButton", "false");
|
||||
await RepairStoreLayoutConfigs(dbContext);
|
||||
await RepairDebugLogConfigs(dbContext);
|
||||
await UpsertGameConfigValue(dbContext, "Friends.PostGamePromptUnderFriendCount", "10");
|
||||
await UpsertGameConfigValue(dbContext, "Friends.SuggestFriendCodeOnFriendsScreenCount", "25");
|
||||
await UpsertGameConfigValue(dbContext, "MobileNotifications.UseFirebasePushNotifications", "true");
|
||||
await UpsertGameConfigValue(dbContext, "UGC.CV2.CooldownBeforeSubscriptionCheckSeconds", "240");
|
||||
await UnblockImportedRooms(dbContext);
|
||||
await EnableBetaCreativeToolsConfigs(dbContext);
|
||||
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
static async Task EnsureSplitTestHardOverrides(RecDBContext dbContext)
|
||||
{
|
||||
var existing = await GameConfigSeeder.FindAsync(dbContext, "splitTestHardOverrides");
|
||||
if (existing == null)
|
||||
{
|
||||
await dbContext.GameConfig.AddAsync(new ConfigVal("splitTestHardOverrides", SplitTestHardOverrides));
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(existing.Value))
|
||||
{
|
||||
existing.Value = SplitTestHardOverrides;
|
||||
return;
|
||||
}
|
||||
|
||||
var entries = existing.Value.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
var known = new HashSet<string>(entries.Select(e => e.Split('=', 2)[0]), StringComparer.OrdinalIgnoreCase);
|
||||
var additions = new List<string>();
|
||||
foreach (var entry in SplitTestHardOverrides.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||
{
|
||||
var name = entry.Split('=', 2)[0];
|
||||
if (known.Add(name))
|
||||
additions.Add(entry);
|
||||
}
|
||||
|
||||
if (additions.Count > 0)
|
||||
{
|
||||
existing.Value = (entries.Concat(additions).ToList()).Aggregate((a, b) => a + ";" + b);
|
||||
}
|
||||
}
|
||||
|
||||
static async Task RepairStoreLayoutConfigs(RecDBContext dbContext)
|
||||
{
|
||||
const string oldLayout = "banner=12;storecampaigns=11;tokenpack=10;allskus=9;hotitem=8;hotinvention=7;newitem=6;mysubscriptioninvention=5;rrplusitem=4;fivestaritem=3;skinsitem=2;hairdyeitem=1;";
|
||||
const string newLayout = "banner=9;hotitem=7;hotinvention=8;newitem=6;mysubscriptioninvention=5;rrplusitem=4;fivestaritem=3;skinsitem=2;hairdyeitem=1;";
|
||||
|
||||
var keys = new[] { "OldStoreLayout", "NewStoreLayout" };
|
||||
var configs = await dbContext.GameConfig.Where(x => keys.Contains(x.Key)).ToListAsync();
|
||||
|
||||
foreach (var config in configs)
|
||||
{
|
||||
var correct = config.Key == "OldStoreLayout" ? oldLayout : newLayout;
|
||||
if (string.Equals(config.Value, correct, StringComparison.Ordinal)) continue;
|
||||
|
||||
var value = config.Value ?? string.Empty;
|
||||
if (string.IsNullOrWhiteSpace(value) ||
|
||||
string.Equals(value, "true", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(value, "false", StringComparison.OrdinalIgnoreCase) ||
|
||||
!value.StartsWith("banner=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
config.Value = correct;
|
||||
}
|
||||
}
|
||||
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
static async Task UnblockImportedRooms(RecDBContext dbContext)
|
||||
{
|
||||
const string key = "RoomLoad.BlockedRoomNamesOnLowMemoryDevices";
|
||||
const string importedRoom = "Showdown";
|
||||
|
||||
var config = await dbContext.GameConfig.FirstOrDefaultAsync(x => x.Key == key);
|
||||
if (config == null || string.IsNullOrWhiteSpace(config.Value) ||
|
||||
!config.Value.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)
|
||||
.Contains(importedRoom, StringComparer.OrdinalIgnoreCase))
|
||||
return;
|
||||
|
||||
config.Value = string.Join(",", config.Value
|
||||
.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)
|
||||
.Where(name => !name.Equals(importedRoom, StringComparison.OrdinalIgnoreCase)));
|
||||
}
|
||||
|
||||
static async Task RepairDebugLogConfigs(RecDBContext dbContext)
|
||||
{
|
||||
const string supportedFlags = "Platform|BootSequence";
|
||||
|
||||
var flags = await dbContext.GameConfig.FirstOrDefaultAsync(x => x.Key == "Debug.AdditionalTraceFlags");
|
||||
if (flags != null &&
|
||||
(flags.Value?.Contains("DissonanceLogs", StringComparison.OrdinalIgnoreCase) == true ||
|
||||
flags.Value?.Contains("Audio", StringComparison.OrdinalIgnoreCase) == true ||
|
||||
flags.Value?.Contains("TachyonClient", StringComparison.OrdinalIgnoreCase) == true))
|
||||
{
|
||||
flags.Value = supportedFlags;
|
||||
}
|
||||
|
||||
var traceProbability = await dbContext.GameConfig.FirstOrDefaultAsync(x => x.Key == "Debug.TraceProbability");
|
||||
if (traceProbability != null && traceProbability.Value == "1")
|
||||
traceProbability.Value = "0";
|
||||
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
static async Task RepairAvatarBootData(RecDBContext dbContext)
|
||||
{
|
||||
const string repairedOutfitSelection = "";
|
||||
const string malformedUnlock = "d0a9262f-5504-46a7-bb10-7507503db58e,95e4cc30-cb68-473d-a395-feadf5b51512,,";
|
||||
const string repairedUnlock = "d0a9262f-5504-46a7-bb10-7507503db58e,95e4cc30-cb68-473d-a395-feadf5b51512,0440f08f-ef1d-49d8-942b-523056e8bb45,";
|
||||
string[] malformedOutfitSelections =
|
||||
[
|
||||
"d0a9262f-5504-46a7-bb10-7507503db58e,,,,1",
|
||||
"d0a9262f-5504-46a7-bb10-7507503db58e,95e4cc30-cb68-473d-a395-feadf5b51512,,,1",
|
||||
"d0a9262f-5504-46a7-bb10-7507503db58e,95e4cc30-cb68-473d-a395-feadf5b51512,0440f08f-ef1d-49d8-942b-523056e8bb45,,1"
|
||||
];
|
||||
|
||||
var repairedAvatars = 0;
|
||||
foreach (var avatar in await dbContext.Avatars.ToListAsync())
|
||||
{
|
||||
var changed = false;
|
||||
foreach (var malformedOutfitSelection in malformedOutfitSelections)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(avatar.Avatar) &&
|
||||
avatar.Avatar.Contains(malformedOutfitSelection, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
avatar.Avatar = avatar.Avatar.Replace(
|
||||
malformedOutfitSelection,
|
||||
repairedOutfitSelection,
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(avatar.Outfits) &&
|
||||
avatar.Outfits.Contains(malformedOutfitSelection, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
avatar.Outfits = avatar.Outfits.Replace(
|
||||
malformedOutfitSelection,
|
||||
repairedOutfitSelection,
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) repairedAvatars++;
|
||||
}
|
||||
|
||||
var repairedUnlockRows = 0;
|
||||
foreach (var unlocks in await dbContext.Unlocks.ToListAsync())
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(unlocks.AvatarItems) ||
|
||||
!unlocks.AvatarItems.Contains(malformedUnlock, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
unlocks.AvatarItems = unlocks.AvatarItems.Replace(
|
||||
malformedUnlock,
|
||||
repairedUnlock,
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
repairedUnlockRows++;
|
||||
}
|
||||
|
||||
if (repairedAvatars > 0 || repairedUnlockRows > 0)
|
||||
{
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
static Task UpsertGameConfigValue(RecDBContext dbContext, string key, string value)
|
||||
=> GameConfigSeeder.UpsertAsync(dbContext, key, value);
|
||||
|
||||
static async Task EnsureReferenceGameConfigs(RecDBContext dbContext)
|
||||
{
|
||||
var existingKeys = await dbContext.GameConfig
|
||||
.Select(x => x.Key)
|
||||
.ToHashSetAsync(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var tracked in dbContext.ChangeTracker.Entries<ConfigVal>())
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(tracked.Entity.Key))
|
||||
existingKeys.Add(tracked.Entity.Key);
|
||||
}
|
||||
|
||||
var added = 0;
|
||||
|
||||
using var doc = JsonDocument.Parse(DBConfig.PlaceholderConfig);
|
||||
foreach (var item in doc.RootElement.EnumerateArray())
|
||||
{
|
||||
if (!item.TryGetProperty("Key", out var keyElement)) continue;
|
||||
var key = keyElement.GetString();
|
||||
if (string.IsNullOrWhiteSpace(key) || !existingKeys.Add(key)) continue;
|
||||
|
||||
var value = item.TryGetProperty("Value", out var valueElement)
|
||||
? valueElement.GetString() ?? string.Empty
|
||||
: string.Empty;
|
||||
|
||||
await dbContext.GameConfig.AddAsync(new ConfigVal(key, value));
|
||||
added++;
|
||||
}
|
||||
|
||||
if (added > 0)
|
||||
{
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
static async Task RepairSplitTestConfigs(RecDBContext dbContext)
|
||||
{
|
||||
var desiredValues = new Dictionary<string, string?>
|
||||
{
|
||||
["splitTestHardOverrides"] = ReferenceSplitTestValue("splitTestHardOverrides"),
|
||||
["splitTestsAvailable"] = ReferenceSplitTestValue("splitTestsAvailable"),
|
||||
["splitTestSegmentProbabilities"] = ReferenceSplitTestValue("splitTestSegmentProbabilities"),
|
||||
};
|
||||
|
||||
var seeded = 0;
|
||||
foreach (var (key, value) in desiredValues)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value)) continue;
|
||||
|
||||
var config = await GameConfigSeeder.FindAsync(dbContext, key);
|
||||
if (config == null)
|
||||
{
|
||||
await dbContext.GameConfig.AddAsync(new ConfigVal(key, value));
|
||||
seeded++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(config.Value)) continue;
|
||||
|
||||
config.Value = value;
|
||||
seeded++;
|
||||
}
|
||||
|
||||
if (seeded > 0)
|
||||
{
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
static async Task DisableCommunityBoardHowToBuildLink(RecDBContext dbContext)
|
||||
{
|
||||
const string key = "CommunityBoard.HowToBuildLink";
|
||||
var config = await GameConfigSeeder.FindAsync(dbContext, key);
|
||||
if (config == null || string.IsNullOrEmpty(config.Value)) return;
|
||||
if (config.Value.IndexOf("youtube", StringComparison.OrdinalIgnoreCase) < 0) return;
|
||||
|
||||
config.Value = string.Empty;
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
static async Task EnableBetaCreativeToolsConfigs(RecDBContext dbContext)
|
||||
{
|
||||
var betaFlags = new Dictionary<string, string>
|
||||
{
|
||||
["UGC.Room2Enabled"] = "true",
|
||||
["UGC.AllowBetaInvertedTubeCreation"] = "true",
|
||||
["UGC.AllowNonBetaInvertedTubeCreation"] = "true",
|
||||
["UGC.EnableInventionsInBetaRooms"] = "true",
|
||||
["UGC.Room2PublishingEnabled"] = "true",
|
||||
["UGC.Room2InventionsEnabled"] = "true",
|
||||
["UGC.Room2InventionAllowBetaPublishing"] = "true",
|
||||
["UGC.Room2InventionPublishingEnabled"] = "true",
|
||||
};
|
||||
|
||||
var updated = 0;
|
||||
foreach (var (key, value) in betaFlags)
|
||||
{
|
||||
var existing = await GameConfigSeeder.FindAsync(dbContext, key);
|
||||
if (existing == null)
|
||||
{
|
||||
await dbContext.GameConfig.AddAsync(new ConfigVal(key, value));
|
||||
updated++;
|
||||
}
|
||||
else if (!string.Equals(existing.Value, value, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
existing.Value = value;
|
||||
updated++;
|
||||
}
|
||||
}
|
||||
|
||||
if (updated > 0)
|
||||
{
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
static string? ReferenceSplitTestValue(string key)
|
||||
{
|
||||
try
|
||||
{
|
||||
var entries = JsonSerializer.Deserialize<List<ConfigVal>>(DBConfig.PlaceholderConfig);
|
||||
return entries?.FirstOrDefault(x => string.Equals(x.Key, key, StringComparison.Ordinal))?.Value;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static async Task RepairPlayerSettingsBootData(RecDBContext dbContext)
|
||||
{
|
||||
const string neutralSplitTestAssignments = "1|{}";
|
||||
var repairedRows = 0;
|
||||
|
||||
foreach (var settingsRow in await dbContext.PlayerSettings.ToListAsync())
|
||||
{
|
||||
var settings = settingsRow.DeserializeSettings();
|
||||
var splitAssignments = settings.FirstOrDefault(x =>
|
||||
string.Equals(x.Key, "SplitTestAssignedSegments", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (splitAssignments == null)
|
||||
{
|
||||
settings.Add(new Setting
|
||||
{
|
||||
Key = "SplitTestAssignedSegments",
|
||||
Value = neutralSplitTestAssignments
|
||||
});
|
||||
repairedRows++;
|
||||
}
|
||||
else if (!string.Equals(splitAssignments.Value, neutralSplitTestAssignments, StringComparison.Ordinal))
|
||||
{
|
||||
splitAssignments.Value = neutralSplitTestAssignments;
|
||||
repairedRows++;
|
||||
}
|
||||
|
||||
settingsRow.Settings = settings.ToJson();
|
||||
}
|
||||
|
||||
if (repairedRows > 0)
|
||||
{
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
static async Task EnsureRoomDeveloperOwnedColumn(RecDBContext dbContext)
|
||||
{
|
||||
await dbContext.Database.ExecuteSqlRawAsync(@"
|
||||
IF COL_LENGTH('dbo.Rooms', 'IsDeveloperOwned') IS NULL
|
||||
ALTER TABLE [Rooms] ADD [IsDeveloperOwned] bit NOT NULL CONSTRAINT [DF_Rooms_IsDeveloperOwned] DEFAULT 0;");
|
||||
}
|
||||
|
||||
static async Task EnsureRoomJoinGiftColumns(RecDBContext dbContext)
|
||||
{
|
||||
await dbContext.Database.ExecuteSqlRawAsync(@"
|
||||
IF COL_LENGTH('dbo.Rooms', 'JoinGiftAvatarItem') IS NULL ALTER TABLE [Rooms] ADD [JoinGiftAvatarItem] nvarchar(max) NOT NULL DEFAULT N'';
|
||||
IF COL_LENGTH('dbo.Rooms', 'JoinGiftAvatarItemType') IS NULL ALTER TABLE [Rooms] ADD [JoinGiftAvatarItemType] int NOT NULL DEFAULT 0;
|
||||
IF COL_LENGTH('dbo.Rooms', 'JoinGiftConsumableItem') IS NULL ALTER TABLE [Rooms] ADD [JoinGiftConsumableItem] nvarchar(max) NOT NULL DEFAULT N'';
|
||||
IF COL_LENGTH('dbo.Rooms', 'JoinGiftContext') IS NULL ALTER TABLE [Rooms] ADD [JoinGiftContext] int NOT NULL DEFAULT 2;
|
||||
IF COL_LENGTH('dbo.Rooms', 'JoinGiftCurrencyAmount') IS NULL ALTER TABLE [Rooms] ADD [JoinGiftCurrencyAmount] int NOT NULL DEFAULT 0;
|
||||
IF COL_LENGTH('dbo.Rooms', 'JoinGiftCurrencyType') IS NULL ALTER TABLE [Rooms] ADD [JoinGiftCurrencyType] int NOT NULL DEFAULT 1;
|
||||
IF COL_LENGTH('dbo.Rooms', 'JoinGiftEnabled') IS NULL ALTER TABLE [Rooms] ADD [JoinGiftEnabled] bit NOT NULL DEFAULT 0;
|
||||
IF COL_LENGTH('dbo.Rooms', 'JoinGiftEquipmentItem') IS NULL ALTER TABLE [Rooms] ADD [JoinGiftEquipmentItem] nvarchar(max) NOT NULL DEFAULT N'';
|
||||
IF COL_LENGTH('dbo.Rooms', 'JoinGiftEquipmentModificationGuid') IS NULL ALTER TABLE [Rooms] ADD [JoinGiftEquipmentModificationGuid] nvarchar(max) NOT NULL DEFAULT N'';
|
||||
IF COL_LENGTH('dbo.Rooms', 'JoinGiftFromPlayerId') IS NULL ALTER TABLE [Rooms] ADD [JoinGiftFromPlayerId] bigint NOT NULL DEFAULT 0;
|
||||
IF COL_LENGTH('dbo.Rooms', 'JoinGiftMessage') IS NULL ALTER TABLE [Rooms] ADD [JoinGiftMessage] nvarchar(max) NOT NULL DEFAULT N'Thanks for visiting!';
|
||||
IF COL_LENGTH('dbo.Rooms', 'JoinGiftOncePerPlayer') IS NULL ALTER TABLE [Rooms] ADD [JoinGiftOncePerPlayer] bit NOT NULL DEFAULT 1;
|
||||
IF COL_LENGTH('dbo.Rooms', 'JoinGiftRarity') IS NULL ALTER TABLE [Rooms] ADD [JoinGiftRarity] int NOT NULL DEFAULT 0;
|
||||
IF COL_LENGTH('dbo.Rooms', 'JoinGiftTitle') IS NULL ALTER TABLE [Rooms] ADD [JoinGiftTitle] nvarchar(max) NOT NULL DEFAULT N'Room Visit Gift';
|
||||
IF COL_LENGTH('dbo.Rooms', 'JoinGiftVersion') IS NULL ALTER TABLE [Rooms] ADD [JoinGiftVersion] int NOT NULL DEFAULT 1;
|
||||
IF COL_LENGTH('dbo.Rooms', 'JoinGiftXp') IS NULL ALTER TABLE [Rooms] ADD [JoinGiftXp] int NOT NULL DEFAULT 0;
|
||||
");
|
||||
|
||||
// A room's join gift boxes, one row each. The Rooms row above still describes the first box,
|
||||
// so a room saved before this table existed keeps handing out what it always did.
|
||||
await dbContext.Database.ExecuteSqlRawAsync(@"
|
||||
IF OBJECT_ID('dbo.RoomJoinGifts', 'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE [RoomJoinGifts] (
|
||||
[RoomJoinGiftId] bigint NOT NULL IDENTITY(1,1),
|
||||
[RoomId] bigint NOT NULL,
|
||||
[SortOrder] int NOT NULL DEFAULT 0,
|
||||
[Version] int NOT NULL DEFAULT 1,
|
||||
[Enabled] bit NOT NULL DEFAULT 1,
|
||||
[OncePerPlayer] bit NOT NULL DEFAULT 1,
|
||||
[Title] nvarchar(max) NOT NULL DEFAULT N'Room Visit Gift',
|
||||
[Message] nvarchar(max) NOT NULL DEFAULT N'Thanks for visiting!',
|
||||
[AvatarItem] nvarchar(max) NOT NULL DEFAULT N'',
|
||||
[AvatarItemType] int NOT NULL DEFAULT 0,
|
||||
[EquipmentItem] nvarchar(max) NOT NULL DEFAULT N'',
|
||||
[EquipmentModificationGuid] nvarchar(max) NOT NULL DEFAULT N'',
|
||||
[ConsumableItem] nvarchar(max) NOT NULL DEFAULT N'',
|
||||
[CurrencyType] int NOT NULL DEFAULT 1,
|
||||
[CurrencyAmount] int NOT NULL DEFAULT 0,
|
||||
[Xp] int NOT NULL DEFAULT 0,
|
||||
[Context] int NOT NULL DEFAULT 2,
|
||||
[Rarity] int NOT NULL DEFAULT 0,
|
||||
[FromPlayerId] bigint NOT NULL DEFAULT 0,
|
||||
CONSTRAINT [PK_RoomJoinGifts] PRIMARY KEY ([RoomJoinGiftId])
|
||||
);
|
||||
END
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_RoomJoinGifts_RoomId' AND object_id = OBJECT_ID(N'dbo.RoomJoinGifts'))
|
||||
CREATE INDEX [IX_RoomJoinGifts_RoomId] ON [RoomJoinGifts] ([RoomId]);
|
||||
");
|
||||
}
|
||||
|
||||
static async Task EnsurePlayerEventColumns(RecDBContext dbContext)
|
||||
{
|
||||
await dbContext.Database.ExecuteSqlRawAsync(@"
|
||||
IF OBJECT_ID('dbo.PlayerEvents', 'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE [PlayerEvents] (
|
||||
[PlayerEventId] bigint NOT NULL IDENTITY(1,1),
|
||||
[Name] nvarchar(max) NOT NULL,
|
||||
[Description] nvarchar(max) NOT NULL,
|
||||
[ImageName] nvarchar(max) NOT NULL,
|
||||
[StartTime] datetime2 NOT NULL,
|
||||
[EndTime] datetime2 NOT NULL,
|
||||
[CreatorPlayerId] bigint NOT NULL,
|
||||
[AttendeeCount] int NOT NULL,
|
||||
[RoomId] bigint NOT NULL,
|
||||
[Accessibility] int NOT NULL,
|
||||
[CreatedAt] datetime2 NOT NULL,
|
||||
[SubRoomId] bigint NULL,
|
||||
[ModerationState] int NOT NULL DEFAULT 0,
|
||||
[ClubId] bigint NULL,
|
||||
[IsMultiInstance] bit NOT NULL DEFAULT 0,
|
||||
[SupportMultiInstanceRoomChat] bit NOT NULL DEFAULT 0,
|
||||
[DefaultBroadcastPermissions] int NOT NULL DEFAULT 0,
|
||||
[CanRequestBroadcastPermissions] int NOT NULL DEFAULT 0,
|
||||
[BroadcastingRoomInstanceId] bigint NULL,
|
||||
[TagsJson] nvarchar(max) NOT NULL DEFAULT N'[]',
|
||||
CONSTRAINT [PK_PlayerEvents] PRIMARY KEY ([PlayerEventId])
|
||||
);
|
||||
END
|
||||
|
||||
IF OBJECT_ID('dbo.PlayerEventResponses', 'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE [PlayerEventResponses] (
|
||||
[PlayerEventResponseId] bigint NOT NULL IDENTITY(1,1),
|
||||
[PlayerEventId] bigint NOT NULL,
|
||||
[PlayerId] bigint NOT NULL,
|
||||
[Type] int NOT NULL,
|
||||
[CreatedAt] datetime2 NOT NULL,
|
||||
CONSTRAINT [PK_PlayerEventResponses] PRIMARY KEY ([PlayerEventResponseId])
|
||||
);
|
||||
END
|
||||
|
||||
IF COL_LENGTH('dbo.PlayerEvents', 'SubRoomId') IS NULL ALTER TABLE [PlayerEvents] ADD [SubRoomId] bigint NULL;
|
||||
IF COL_LENGTH('dbo.PlayerEvents', 'ModerationState') IS NULL ALTER TABLE [PlayerEvents] ADD [ModerationState] int NOT NULL DEFAULT 0;
|
||||
IF COL_LENGTH('dbo.PlayerEvents', 'ClubId') IS NULL ALTER TABLE [PlayerEvents] ADD [ClubId] bigint NULL;
|
||||
IF COL_LENGTH('dbo.PlayerEvents', 'IsMultiInstance') IS NULL ALTER TABLE [PlayerEvents] ADD [IsMultiInstance] bit NOT NULL DEFAULT 0;
|
||||
IF COL_LENGTH('dbo.PlayerEvents', 'SupportMultiInstanceRoomChat') IS NULL ALTER TABLE [PlayerEvents] ADD [SupportMultiInstanceRoomChat] bit NOT NULL DEFAULT 0;
|
||||
IF COL_LENGTH('dbo.PlayerEvents', 'DefaultBroadcastPermissions') IS NULL ALTER TABLE [PlayerEvents] ADD [DefaultBroadcastPermissions] int NOT NULL DEFAULT 0;
|
||||
IF COL_LENGTH('dbo.PlayerEvents', 'CanRequestBroadcastPermissions') IS NULL ALTER TABLE [PlayerEvents] ADD [CanRequestBroadcastPermissions] int NOT NULL DEFAULT 0;
|
||||
IF COL_LENGTH('dbo.PlayerEvents', 'BroadcastingRoomInstanceId') IS NULL ALTER TABLE [PlayerEvents] ADD [BroadcastingRoomInstanceId] bigint NULL;
|
||||
IF COL_LENGTH('dbo.PlayerEvents', 'TagsJson') IS NULL ALTER TABLE [PlayerEvents] ADD [TagsJson] nvarchar(max) NOT NULL DEFAULT N'[]';
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM __EFMigrationsHistory WHERE MigrationId = '20260805082102_FixModelChanges')
|
||||
INSERT INTO __EFMigrationsHistory (MigrationId, ProductVersion) VALUES ('20260805082102_FixModelChanges', '9.0.11');
|
||||
IF NOT EXISTS (SELECT 1 FROM __EFMigrationsHistory WHERE MigrationId = '20260806004625_PlayerEventResponses')
|
||||
INSERT INTO __EFMigrationsHistory (MigrationId, ProductVersion) VALUES ('20260806004625_PlayerEventResponses', '9.0.11');
|
||||
IF NOT EXISTS (SELECT 1 FROM __EFMigrationsHistory WHERE MigrationId = '20260814120000_AddPlayerEventModerationFields')
|
||||
INSERT INTO __EFMigrationsHistory (MigrationId, ProductVersion) VALUES ('20260814120000_AddPlayerEventModerationFields', '9.0.11');
|
||||
IF NOT EXISTS (SELECT 1 FROM __EFMigrationsHistory WHERE MigrationId = '20260926120000_AddPlayerEventClubMetadata')
|
||||
INSERT INTO __EFMigrationsHistory (MigrationId, ProductVersion) VALUES ('20260926120000_AddPlayerEventClubMetadata', '9.0.11');
|
||||
IF NOT EXISTS (SELECT 1 FROM __EFMigrationsHistory WHERE MigrationId = '20261008120000_AddPlayerEventBroadcastingRoomInstance')
|
||||
INSERT INTO __EFMigrationsHistory (MigrationId, ProductVersion) VALUES ('20261008120000_AddPlayerEventBroadcastingRoomInstance', '9.0.11');
|
||||
");
|
||||
}
|
||||
|
||||
|
||||
static async Task EnsureVoiceChatRestrictionTable(RecDBContext dbContext)
|
||||
{
|
||||
await dbContext.Database.ExecuteSqlRawAsync(@"
|
||||
IF OBJECT_ID('dbo.VoiceChatRestrictions', 'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE [VoiceChatRestrictions] (
|
||||
[Id] bigint NOT NULL IDENTITY(1,1),
|
||||
[PlayerId] bigint NOT NULL,
|
||||
[Reason] nvarchar(max) NOT NULL DEFAULT N'',
|
||||
[Category] int NOT NULL DEFAULT 0,
|
||||
[StartedAt] datetime2 NOT NULL,
|
||||
[ExpiresAt] datetime2 NOT NULL,
|
||||
[IssuedBy] nvarchar(max) NOT NULL DEFAULT N'Admin Panel',
|
||||
[IssuedByUsername] nvarchar(max) NULL,
|
||||
[ModeratorNote] nvarchar(max) NULL,
|
||||
[ClearedAt] datetime2 NULL,
|
||||
[ClearedBy] nvarchar(max) NULL,
|
||||
CONSTRAINT [PK_VoiceChatRestrictions] PRIMARY KEY ([Id])
|
||||
);
|
||||
CREATE INDEX [IX_VoiceChatRestrictions_PlayerId] ON [VoiceChatRestrictions] ([PlayerId]);
|
||||
END
|
||||
");
|
||||
}
|
||||
|
||||
static async Task EnsureValidSceneLocations(RecDBContext dbContext)
|
||||
{
|
||||
var subRooms = await dbContext.SubRooms.ToListAsync();
|
||||
if (subRooms.Count == 0) return;
|
||||
|
||||
var roomIds = subRooms.Select(s => s.RoomId).Distinct().ToList();
|
||||
var rooms = await dbContext.Rooms
|
||||
.Where(r => roomIds.Contains(r.RoomId))
|
||||
.ToDictionaryAsync(r => r.RoomId);
|
||||
|
||||
var byRoom = subRooms
|
||||
.GroupBy(s => s.RoomId)
|
||||
.ToDictionary(g => g.Key, g => g.ToList());
|
||||
|
||||
int repaired = 0;
|
||||
foreach (var subRoom in subRooms)
|
||||
{
|
||||
if (SceneLocations.IsValid(subRoom.UnitySceneId)) continue;
|
||||
|
||||
bool isDorm = rooms.TryGetValue(subRoom.RoomId, out var room) && room.IsDorm;
|
||||
var siblings = byRoom.TryGetValue(subRoom.RoomId, out var list)
|
||||
? list.Select(s => s.UnitySceneId)
|
||||
: Enumerable.Empty<string?>();
|
||||
|
||||
subRoom.UnitySceneId = SceneLocations.ResolveFallback(siblings, isDorm);
|
||||
repaired++;
|
||||
}
|
||||
|
||||
if (repaired > 0)
|
||||
{
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
static async Task EnsureCatalogTables(RecDBContext dbContext)
|
||||
{
|
||||
await dbContext.Database.ExecuteSqlRawAsync(@"
|
||||
IF OBJECT_ID('dbo.ItemSets', 'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE [ItemSets] (
|
||||
[ItemSetId] bigint NOT NULL IDENTITY(1,1),
|
||||
[FriendlyName] nvarchar(max) NOT NULL DEFAULT N'',
|
||||
CONSTRAINT [PK_ItemSets] PRIMARY KEY ([ItemSetId])
|
||||
);
|
||||
END
|
||||
|
||||
IF OBJECT_ID('dbo.Tags', 'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE [Tags] (
|
||||
[TagId] bigint NOT NULL IDENTITY(1,1),
|
||||
[Tag] nvarchar(max) NOT NULL DEFAULT N'',
|
||||
[Description] nvarchar(max) NOT NULL DEFAULT N'',
|
||||
CONSTRAINT [PK_Tags] PRIMARY KEY ([TagId])
|
||||
);
|
||||
END
|
||||
");
|
||||
}
|
||||
|
||||
internal sealed class CountingStream : Stream
|
||||
{
|
||||
private readonly Stream _inner;
|
||||
private long _bytes;
|
||||
private readonly System.Collections.Generic.List<byte> _captured = new();
|
||||
public long BytesWritten => _bytes;
|
||||
public byte[] Peek => _captured.ToArray();
|
||||
|
||||
public CountingStream(Stream inner) => _inner = inner;
|
||||
|
||||
public override bool CanRead => _inner.CanRead;
|
||||
public override bool CanSeek => _inner.CanSeek;
|
||||
public override bool CanWrite => _inner.CanWrite;
|
||||
public override long Length => _inner.Length;
|
||||
public override long Position { get => _inner.Position; set => _inner.Position = value; }
|
||||
public override void Flush() => _inner.Flush();
|
||||
public override Task FlushAsync(CancellationToken ct) => _inner.FlushAsync(ct);
|
||||
public override int Read(byte[] buffer, int offset, int count) => _inner.Read(buffer, offset, count);
|
||||
public override long Seek(long offset, SeekOrigin origin) => _inner.Seek(offset, origin);
|
||||
public override void SetLength(long value) => _inner.SetLength(value);
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count)
|
||||
{
|
||||
_inner.Write(buffer, offset, count);
|
||||
_bytes += count;
|
||||
if (_captured.Count < 8)
|
||||
{
|
||||
var take = Math.Min(count, 8 - _captured.Count);
|
||||
for (var i = 0; i < take; i++) _captured.Add(buffer[offset + i]);
|
||||
}
|
||||
}
|
||||
|
||||
public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken ct)
|
||||
{
|
||||
await _inner.WriteAsync(buffer, offset, count, ct);
|
||||
_bytes += count;
|
||||
if (_captured.Count < 8)
|
||||
{
|
||||
var take = Math.Min(count, 8 - _captured.Count);
|
||||
for (var i = 0; i < take; i++) _captured.Add(buffer[offset + i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user