Add blazor & MVC Support (#602)

* Add initial code for Razor pages support

* Remove finalizer

* Try fully loading blazor

This is most likely entirely broken because of a rebase now

* UseSptBlazor after app.Use

* Fix up StaticWebAsset loading, add MudBlazor

* Implement page

* Update comment

* Replaced existing status page with razor

* Track background video in LFS

* Update attributes

* Improved status page theming

* Fix up wwwroot publish folder to SPT_Data/wwwroot

* Added name to page

* Remove unnecessary code

* Begin fixing up MVC & Blazor for modding

* Update TestMod

* Cleanup todo

* Further work out mod support

* Re-order initialization and use logger

* Rename library to SPTarkov.Server.Web

---------

Co-authored-by: Chomp <dev@dev.sp-tarkov.com>
Co-authored-by: Chomp <27521899+chompDev@users.noreply.github.com>
This commit is contained in:
Jesse
2025-10-02 21:03:27 +02:00
committed by GitHub
parent cf29c7bde3
commit 687b4f7a49
23 changed files with 820 additions and 73 deletions
+1
View File
@@ -3,3 +3,4 @@
# LFS tracking for large JSON files and all images # LFS tracking for large JSON files and all images
Libraries/SPTarkov.Server.Assets/SPT_Data/database/locations/**/looseLoot.json filter=lfs diff=lfs merge=lfs -text Libraries/SPTarkov.Server.Assets/SPT_Data/database/locations/**/looseLoot.json filter=lfs diff=lfs merge=lfs -text
Libraries/SPTarkov.Server.Assets/SPT_Data/database/templates/items.json filter=lfs diff=lfs merge=lfs -text Libraries/SPTarkov.Server.Assets/SPT_Data/database/templates/items.json filter=lfs diff=lfs merge=lfs -text
Libraries/SPTarkov.Server.Web/wwwroot/thank-you/background.mp4 filter=lfs diff=lfs merge=lfs -text
@@ -1,55 +0,0 @@
using System.Text;
using Microsoft.AspNetCore.Http;
using SPTarkov.DI.Annotations;
using SPTarkov.Server.Core.Models.Common;
using SPTarkov.Server.Core.Models.Spt.Config;
using SPTarkov.Server.Core.Servers;
using SPTarkov.Server.Core.Servers.Http;
using SPTarkov.Server.Core.Services;
using SPTarkov.Server.Core.Utils;
namespace SPTarkov.Server.Core.Status;
[Injectable(TypePriority = 0)]
public class StatusPage(TimeUtil timeUtil, ProfileActivityService profileActivityService, ConfigServer configServer) : IHttpListener
{
protected readonly CoreConfig CoreConfig = configServer.GetConfig<CoreConfig>();
public bool CanHandle(MongoId sessionId, HttpContext context)
{
return context.Request.Method == "GET" && context.Request.Path.Value.Contains("/status");
}
public async Task Handle(MongoId sessionId, HttpContext context)
{
var resp = context.Response;
var sptVersion = $"SPT version: {ProgramStatics.SPT_VERSION()}";
var debugEnabled = $"Debug enabled: {ProgramStatics.DEBUG()}";
var modsEnabled = $"Mods enabled: {ProgramStatics.MODS()}";
var timeStarted = $"Started : {timeUtil.GetDateTimeFromTimeStamp(CoreConfig.ServerStartTime.Value)}";
var uptime = $"Uptime: {DateTimeOffset.UtcNow.ToUnixTimeSeconds() - CoreConfig.ServerStartTime} seconds".ToArray();
var activeProfiles = profileActivityService.GetActiveProfileIdsWithinMinutes(30);
var activePlayerCount = $"Profiles active in last 30 minutes: {activeProfiles.Count}. {string.Join(",", activeProfiles)}";
resp.StatusCode = 200;
resp.ContentType = "text/html";
await resp.Body.WriteAsync(Encoding.ASCII.GetBytes(sptVersion));
await resp.Body.WriteAsync(Encoding.ASCII.GetBytes("<br>"));
await resp.Body.WriteAsync(Encoding.ASCII.GetBytes(debugEnabled));
await resp.Body.WriteAsync(Encoding.ASCII.GetBytes("<br>"));
await resp.Body.WriteAsync(Encoding.ASCII.GetBytes(modsEnabled));
await resp.Body.WriteAsync(Encoding.ASCII.GetBytes("<br>"));
await resp.Body.WriteAsync(Encoding.ASCII.GetBytes(timeStarted));
await resp.Body.WriteAsync(Encoding.ASCII.GetBytes("<br>"));
await resp.Body.WriteAsync(Encoding.ASCII.GetBytes(uptime));
await resp.Body.WriteAsync(Encoding.ASCII.GetBytes("<br>"));
await resp.Body.WriteAsync(Encoding.ASCII.GetBytes(activePlayerCount));
await resp.Body.WriteAsync(Encoding.ASCII.GetBytes("<br>"));
await resp.StartAsync();
await resp.CompleteAsync();
}
}
@@ -15,11 +15,6 @@ public class SptLogger<T> : ISptLogger<T>, IDisposable
private const string ConfigurationPathDev = "./sptLogger.Development.json"; private const string ConfigurationPathDev = "./sptLogger.Development.json";
private SptLoggerConfiguration _config; private SptLoggerConfiguration _config;
~SptLogger()
{
_loggerQueueManager.DumpAndStop();
}
public SptLogger(FileUtil fileUtil, JsonUtil jsonUtil, SptLoggerQueueManager loggerQueueManager) public SptLogger(FileUtil fileUtil, JsonUtil jsonUtil, SptLoggerQueueManager loggerQueueManager)
{ {
_category = typeof(T).FullName; _category = typeof(T).FullName;
@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<base href="/" />
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<ImportMap />
<HeadOutlet />
</head>
<body>
<Routes/>
<script src="/_framework/blazor.web.js"></script>
</body>
</html>
@code {
}
@@ -0,0 +1,11 @@
@inherits LayoutComponentBase
<div class="page">
<main>
@Body
</main>
</div>
@code {
}
@@ -0,0 +1,20 @@
@inherits LayoutComponentBase
<HeadContent>
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet" />
<link href="@Assets["_content/MudBlazor/MudBlazor.min.css"]" rel="stylesheet" />
</HeadContent>
<MudThemeProvider IsDarkMode=true />
<MudPopoverProvider />
<MudDialogProvider />
<MudSnackbarProvider />
<main>
@Body
<script src="@Assets["_content/MudBlazor/MudBlazor.min.js"]"></script>
</main>
@code {
}
@@ -0,0 +1,27 @@
@page "/example-page"
@using SPTarkov.Server.Web.Components.Layout
@layout BaseMudBlazorLayout
<MudContainer Class="mt-16 d-flex justify-center">
<MudGrid Justify="Justify.Center">
<MudItem xs="12" sm="6" md="4">
<MudCard Elevation="25" Class="rounded-lg pb-4">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h5" Align="Align.Center">SPT 4.0</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<div class="d-flex justify-center">
<MudText Typo="Typo.h5" Class="ml-1 mt-5" Color="Color.Secondary">Is pretty awesome</MudText>
</div>
<MudList T="string" Class="mx-auto mt-4" Style="width:300px;">
<MudListItem Icon="@Icons.Material.Filled.Star">Feature 1</MudListItem>
<MudListItem Icon="@Icons.Material.Filled.Star">Feature 2</MudListItem>
<MudListItem Icon="@Icons.Material.Filled.Star">Feature 3</MudListItem>
</MudList>
</MudCardContent>
</MudCard>
</MudItem>
</MudGrid>
</MudContainer>
@@ -0,0 +1,110 @@
@page "/status"
@using SPTarkov.Server.Core.Helpers
@using SPTarkov.Server.Core.Models.Spt.Config
@using SPTarkov.Server.Core.Servers
@using SPTarkov.Server.Core.Services
@using SPTarkov.Server.Core.Utils
@using SPTarkov.Server.Web.Components.Layout
@inject ConfigServer ConfigServer
@inject TimeUtil TimeUtil
@inject ProfileActivityService ProfileActivityService
@inject ProfileHelper ProfileHelper
@layout BaseMudBlazorLayout
<HeadContent>
<link href="@Assets["_content/MudBlazor/MudBlazor.min.css"]" rel="stylesheet" />
<meta name="robots" content="noindex, nofollow">
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300..800;1,300..800&amp;family=Roboto:ital,wght@0,100..900;1,100..900&amp;display=swap" rel="stylesheet">
</HeadContent>
<MudThemeProvider Theme="@_theme" IsDarkMode="true" />
<MudContainer Class="mt-16 d-flex justify-center">
<MudGrid Justify="Justify.Center">
<MudItem xs="12" sm="6" md="4">
<MudCard Elevation="25" Class="rounded-lg pb-4">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h4" Color="Color.Warning" Align="Align.Center">Server health</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudList T="string" Class="mx-auto mt-4">
<MudListItem Icon="@Icons.Material.Filled.Circle" IconColor="Color.Warning">SPT Version: @_sptVersion</MudListItem>
<MudListItem Icon="@Icons.Material.Filled.Circle" IconColor="Color.Warning">Mods @(_modsEnabled ? "ENABLED" : "DISABLED")</MudListItem>
<MudListItem Icon="@Icons.Material.Filled.Circle" IconColor="Color.Warning">Debug @(_debugEnabled ? "ENABLED" : "DISABLED")</MudListItem>
<MudListItem Icon="@Icons.Material.Filled.Circle" IconColor="Color.Warning">Time started: @_startTime</MudListItem>
<MudListItem Icon="@Icons.Material.Filled.Circle" IconColor="Color.Warning">Uptime: @_uptimeSeconds seconds</MudListItem>
<MudListItem Icon="@Icons.Material.Filled.Circle" IconColor="Color.Warning">Total profile count: @_totalProfileCount</MudListItem>
<MudListItem Icon="@Icons.Material.Filled.Inbox" IconColor="Color.Warning" Text="Profiles active last 30 minutes:" Expanded>
<NestedList>
@foreach (var profile in _activeProfiles)
{
<MudListItem Icon="@Icons.Material.Filled.Circle" IconColor="Color.Warning">
@profile
</MudListItem>
}
</NestedList>
</MudListItem>
</MudList>
</MudCardContent>
</MudCard>
</MudItem>
</MudGrid>
</MudContainer>
@code
{
private string _sptVersion = string.Empty;
private bool _debugEnabled = false;
private bool _modsEnabled = false;
private DateTime _startTime = DateTime.Now;
private long _uptimeSeconds = 0;
private readonly List<string> _activeProfiles = [];
private int _totalProfileCount = 0;
protected override void OnInitialized()
{
base.OnInitialized();
var coreConfig = ConfigServer.GetConfig<CoreConfig>();
_sptVersion = ProgramStatics.SPT_VERSION().ToString();
_debugEnabled = ProgramStatics.DEBUG();
_modsEnabled = ProgramStatics.MODS();
_startTime = TimeUtil.GetDateTimeFromTimeStamp(coreConfig.ServerStartTime.Value);
_uptimeSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds() - coreConfig.ServerStartTime.Value;
var activeProfileIds = ProfileActivityService.GetActiveProfileIdsWithinMinutes(30);
if (activeProfileIds.Count == 0)
{
_activeProfiles.Add("None");
}
else
{
foreach (var activeProfileId in activeProfileIds)
{
_activeProfiles.Add($"{activeProfileId} ({ProfileHelper.GetPmcProfile(activeProfileId).Info.Nickname})");
}
}
_totalProfileCount = ProfileHelper.GetProfiles().Count;
}
private readonly MudTheme _theme = new()
{
PaletteDark = new PaletteDark
{
Primary = Colors.Blue.Default,
Secondary = Colors.Orange.Default,
Warning = Colors.Amber.Default,
Info = Colors.Blue.Lighten1,
Success = Colors.Green.Default,
Background = "rgba(0,0,0,0.9)",
Surface = "rgba(255,255,255,0.05)",
AppbarBackground = "rgba(0,0,0,0.8)",
TextPrimary = "#FFFFFF"
}
};
}
@@ -0,0 +1,418 @@
@page "/"
@using Microsoft.JSInterop
@using MudBlazor
@using SPTarkov.Server.Web.Components.Layout
@inject IJSRuntime JSRuntime
<PageTitle>Thank You - Single Player Tarkov - Version 4 Contributors</PageTitle>
<HeadContent>
<link href="@Assets["_content/MudBlazor/MudBlazor.min.css"]" rel="stylesheet" />
<meta name="robots" content="noindex, nofollow">
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300..800;1,300..800&amp;family=Roboto:ital,wght@0,100..900;1,100..900&amp;display=swap" rel="stylesheet">
</HeadContent>
<MudThemeProvider Theme="@_theme" IsDarkMode="true" />
<div class="video-background">
<video autoplay muted loop playsinline>
<source src="/SPTarkov.Server.Web/thank-you/background.mp4" type="video/mp4" />
</video>
<div class="video-overlay"></div>
</div>
<MudContainer Class="main-container">
<MudPaper Class="main-content-paper" Square="false">
<MudContainer MaxWidth="MaxWidth.Medium">
<MudStack AlignItems="AlignItems.Center">
<MudStack class="thank-you-section" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h1" Align="Align.Center">
Thank You
</MudText>
<MudText Typo="Typo.h2" Color="Color.Warning" Align="Align.Center">
Version 4 Contributors
</MudText>
</MudStack>
<MudStack>
<MudText Typo="Typo.body1" Align="Align.Center" Class="intro">
The release of Single Player Tarkov Version 4 would not have been possible without the incredible dedication, talent, and passion of our amazing community. From core developers to modders, testers to documentation writers, each person listed here has contributed to making SPT what it is today.
</MudText>
<MudText Typo="Typo.body1" Align="Align.Center" Class="intro">
To everyone who spent countless hours debugging, coding, testing, and supporting the project, this is for you. Your work has transformed the way thousands experience Tarkov, and we are forever grateful.
</MudText>
</MudStack>
<MudStack class="contributor-section">
<MudGrid Justify="Justify.Center">
@foreach (var contributor in ShuffledContributors)
{
<MudItem xs="10" sm="5" md="4">
<MudCard Class="contributor-card" @onclick="() => TriggerConfetti(false, contributor)">
<MudCardContent Class="contributor-card-content">
<MudText Typo="Typo.caption" Align="Align.Center" Class="contributor-name">
@contributor
</MudText>
</MudCardContent>
</MudCard>
</MudItem>
}
</MudGrid>
</MudStack>
<MudText Typo="Typo.body1" Align="Align.Center" Class="closing-text">
Every commit, every bug report, every line of code, and every moment of support has shaped this project. You are the heart of Single Player Tarkov.
</MudText>
<MudText Typo="Typo.body1" Align="Align.Center" Class="patreon-divider">
Patreon Supporters
</MudText>
<MudStack Style="max-width: 800px;">
<MudText Typo="Typo.body1" Align="Align.Center" Class="intro">
A special thank you to our generous Patreon supporters whose financial contributions have helped sustain and grow the SPT project. Your support enables us to maintain infrastructure, develop new features, and keep this project alive for everyone to enjoy.
</MudText>
<MudText Typo="Typo.caption" Align="Align.Center" Class="patreon-disclaimer">
The following list includes supporters who opted to be publicly recognized. To all our supporters not listed here, whether by choice or oversight, please know that your contributions are equally valued and deeply appreciated. Every single patron, visible or not, makes this project possible. ♥️
</MudText>
</MudStack>
<MudStack class="contributor-section patreon-section">
<MudGrid Justify="Justify.Center">
@foreach (var supporter in ShuffledPatreonSupporters)
{
<MudItem xs="10" sm="5" md="4">
<MudCard Class="contributor-card patreon-card" @onclick="() => TriggerConfetti(true, supporter)">
<MudCardContent Class="patreon-card-content">
<MudStack Direction="Row" AlignItems="AlignItems.Center" Justify="Justify.Center">
<MudIcon Icon="@Icons.Material.Filled.Star" Color="Color.Warning" Size="Size.Small" />
<MudText Typo="Typo.caption" Align="Align.Center" Class="patreon-name">
@supporter
</MudText>
</MudStack>
</MudCardContent>
</MudCard>
</MudItem>
}
</MudGrid>
</MudStack>
<MudContainer Class="signature">
<MudText HtmlTag="p" Align="Align.Center">
With deepest gratitude,<br />
The SPT Team
</MudText>
</MudContainer>
</MudStack>
</MudContainer>
</MudPaper>
</MudContainer>
<MudContainer Class="footer-container">
<MudText Typo="Typo.caption">
The <MudText Typo="Typo.caption" Style="font-weight: bold;">SPT</MudText> project is not affiliated with Battlestate Games Ltd. in any way. All trademarks are property of their respective owners.
</MudText>
</MudContainer>
<style>
.video-background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -2;
overflow: hidden;
}
.video-background video {
width: 100%;
height: 100%;
object-fit: cover;
}
.video-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.75);
z-index: -1;
}
.contributor-section {
border: 1px solid rgba(255, 193, 7, 0.1);
background: rgba(0, 0, 0, 0.3);
padding: 2rem 1rem;
}
.patreon-section {
background: linear-gradient(135deg, rgba(255, 193, 7, 0.05), rgba(0, 0, 0, 0.3));
border: 1px solid rgba(255, 193, 7, 0.3);
}
.main-container {
position: relative;
z-index: 1;
padding-top: 2rem;
padding-bottom: 2rem;
min-height: 100vh;
}
.header-section {
margin-bottom: 2rem;
}
.main-content-paper {
background: repeating-linear-gradient(-45deg, rgba(12, 12, 19, 0.6), rgba(12, 12, 19, 0.6) 10px, rgba(15, 15, 24, 0.6) 10px, rgba(15, 15, 24, 0.6) 20px), rgba(0, 0, 0, 0.8);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 193, 7, 0.2);
padding: 2rem 1.5rem;
margin: 2rem auto;
max-width: 95ch;
width: 90%;
}
.closing-text {
font-size: 1.25rem;
font-weight: 300;
margin-bottom: 2rem;
font-style: italic;
}
.thank-you-section h1 {
font-size: 3rem;
font-weight: 100;
margin-bottom: 0.5rem;
background: linear-gradient(45deg, #ffc107, #fff);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.thank-you-section h2 {
font-size: 1.5rem;
font-weight: 300;
margin-bottom: 2rem;
color: #ffc107;
}
.intro-alert,
.patreon-alert {
background: rgba(33, 150, 243, 0.1) !important;
border-color: rgba(33, 150, 243, 0.3) !important;
backdrop-filter: blur(5px);
}
.patreon-alert {
background: rgba(255, 193, 7, 0.1) !important;
border-color: rgba(255, 193, 7, 0.3) !important;
}
.patreon-divider {
font-size: 1.75rem;
font-weight: 300;
margin: 0 0 2rem;
color: #ffc107;
text-transform: uppercase;
position: relative;
padding: 0 2rem;
}
.patreon-divider::before {
left: 0;
}
patreon-divider::after {
right: 0;
}
.patreon-divider::before,
.patreon-divider::after {
content: "";
position: absolute;
top: 50%;
width: calc(50% - 10rem);
height: 1px;
background: linear-gradient(90deg, transparent, rgba(255, 193, 7, 0.5), transparent);
}
.patreon-disclaimer {
font-size: 1rem;
line-height: 1.6;
margin: -0.5rem auto 2rem;
padding: 1rem 2rem;
background: rgba(255, 193, 7, 0.05);
border-left: 3px solid rgba(255, 193, 7, 0.3);
border-right: 3px solid rgba(255, 193, 7, 0.3);
font-style: italic;
font-weight: 300;
color: rgba(255, 255, 255, 0.9);
max-width: 85%;
}
.intro {
font-size: 1.125rem;
line-height: 1.75;
margin-bottom: 2rem;
font-weight: 300;
}
.contributor-card {
background: linear-gradient(135deg, rgba(255, 193, 7, 0.1), rgba(255, 255, 255, 0.05)) !important;
border: 1px solid rgba(255, 193, 7, 0.2) !important;
transition: all 0.3s ease !important;
cursor: pointer;
backdrop-filter: blur(5px);
position: relative;
overflow: hidden;
}
.contributor-card::before {
content: "";
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 193, 7, 0.2), transparent);
transition: left 0.5s ease;
}
.contributor-card:hover {
transform: translateY(-2px);
border-color: rgba(255, 193, 7, 0.5) !important;
box-shadow: 0 4px 12px rgba(255, 193, 7, 0.2) !important;
}
.contributor-card:hover::before {
left: 100%;
}
.contributor-card-content {
padding: 1rem 0.5rem !important;
min-height: 32px;
display: flex;
align-items: center;
justify-content: center;
}
.contributor-name {
color: white !important;
font-weight: 400;
}
.patreon-card {
background: linear-gradient(135deg, rgba(255, 193, 7, 0.15), rgba(255, 255, 255, 0.08)) !important;
border: 1px solid rgba(255, 193, 7, 0.3) !important;
transition: all 0.3s ease !important;
cursor: pointer;
backdrop-filter: blur(5px);
}
.patreon-card:hover {
transform: translateY(-2px);
border-color: rgba(255, 193, 7, 0.7) !important;
box-shadow: 0 4px 16px rgba(255, 193, 7, 0.3) !important;
}
.patreon-card-content {
padding: 1rem 0.5rem !important;
min-height: 48px;
display: flex;
align-items: center;
justify-content: center;
}
.patreon-name {
color: #ffc107 !important;
font-weight: 500;
}
.signature {
font-size: 1.125rem;
margin-top: 3rem;
font-weight: 400;
color: #ffc107;
letter-spacing: 0.05em;
}
.footer-container {
text-align: center;
padding: 1rem;
background: repeating-linear-gradient(-45deg, rgba(12, 12, 19, 0.6), rgba(12, 12, 19, 0.6) 10px, rgba(15, 15, 24, 0.6) 10px, rgba(15, 15, 24, 0.6) 20px), rgba(0, 0, 0, 0.8);
}
</style>
@code {
private readonly List<string> Contributors = new()
{
"5o2", "AdmiralAwsum", "Angel-git", "ArchangelWTF", "BrotherVeren",
"CameronW1", "CJ", "Clodan", "CP89", "CWX", "DanW", "devmaximum",
"Doup22", "DrakiaXYZ", "fearthedje", "GentlemenSausage", "Guidot42",
"HB53", "Hulkhan22", "January", "Kaeno", "Lacyway", "Muramas",
"nailz420", "Parataku", "ParanoiaBruce", "PhantomInTime", "qe201020335",
"R3ality", "Razzmatazz", "Redbeard", "Refringe", "ShadowXtrex",
"Shibdib", "Stealthsuit", "studentchy", "Tetris", "Th3NightHawk",
"TheHeadPhonesGuy", "ThyMuffinMan", "ultragastro", "Valens",
"XeonDead", "yurikus", "Chilly"
};
private readonly List<string> PatreonSupporters = new()
{
"Refringe", "DrakiaXYZ", "Tron", "Bepis", "John Thicc",
"Cardsmen", "Nexstat", "NumberedJester", "Irabeth Tyrabade", "DanW"
};
private List<string> ShuffledContributors = new();
private List<string> ShuffledPatreonSupporters = new();
private readonly MudTheme _theme = new()
{
PaletteDark = new PaletteDark()
{
Primary = Colors.Blue.Default,
Secondary = Colors.Orange.Default,
Warning = Colors.Amber.Default,
Info = Colors.Blue.Lighten1,
Success = Colors.Green.Default,
Background = "rgba(0,0,0,0.9)",
Surface = "rgba(255,255,255,0.05)",
AppbarBackground = "rgba(0,0,0,0.8)",
TextPrimary = "#FFFFFF"
}
};
protected override void OnInitialized()
{
ShuffledContributors = ShuffleList(Contributors);
ShuffledPatreonSupporters = ShuffleList(PatreonSupporters);
}
private async Task TriggerConfetti(bool isPatreon, string name)
{
//Todo: Needs implemnetation
if (isPatreon)
{
}
else
{
}
}
private static List<string> ShuffleList(List<string> list)
{
var shuffled = new List<string>(list);
for (int i = shuffled.Count - 1; i > 0; i--)
{
int j = Random.Shared.Next(i + 1);
(shuffled[i], shuffled[j]) = (shuffled[j], shuffled[i]);
}
return shuffled;
}
}
@@ -0,0 +1,12 @@
@using System.Reflection
@using SPTarkov.Server.Web.Components.Layout
<Router AppAssembly="@typeof(SPTWeb).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(BaseMainLayout)" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
</Router>
@code {
}
@@ -0,0 +1,8 @@
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using MudBlazor;
@@ -0,0 +1,22 @@
namespace SPTarkov.Server.Web;
/// <summary>
/// This empty interface is used as a metadata marker to identify mod assemblies that integrate with Blazor or MVC.
/// </summary>
/// <remarks>
/// Implementing this interface signals to the host application to:
/// <list type="bullet">
/// <item>
/// <description>Link the mod's <c>wwwroot</c> directory, enabling serving of static web assets (CSS, JS, etc.).</description>
/// </item>
/// <item>
/// <description>Register the mod's Blazor components and pages for routing within the application.</description>
/// </item>
/// <item>
/// <description>Register the mod's MVC controllers for use as APIs where necessary.</description>
/// </item>
/// </list>
///
/// This interface is intentionally empty but may be extended in the future to include additional metadata.
/// </remarks>
public interface IModWebMetadata { }
+76
View File
@@ -0,0 +1,76 @@
using Microsoft.Extensions.FileProviders;
using MudBlazor.Services;
using SPTarkov.Server.Core.Models.Spt.Mod;
using SPTarkov.Server.Web.Components;
namespace SPTarkov.Server.Web;
public static class SPTWeb
{
internal static IEnumerable<SptMod> SptWebMods = [];
public static void InitializeSptBlazor(this WebApplicationBuilder builder, IReadOnlyList<SptMod> sptMods)
{
SptWebMods = sptMods.Where(mod => mod.ModMetadata is IModWebMetadata).ToList();
builder.WebHost.UseStaticWebAssets();
builder.Services.AddMudServices();
builder.Services.AddRazorComponents().AddInteractiveServerComponents();
var mvcBuilder = builder.Services.AddControllers();
foreach (var assembly in SptWebMods.SelectMany(mod => mod.Assemblies))
{
mvcBuilder.AddApplicationPart(assembly);
}
}
public static void UseSptBlazor(this WebApplication app)
{
var logger = app.Services.GetRequiredService<ILogger<App>>();
app.UseAntiforgery();
#if DEBUG
//MS currently has a bug where streaming video doesn't work properly in debug, unless you use this
//Issue: https://github.com/dotnet/aspnetcore/issues/63320
app.UseStaticFiles();
#else
app.MapStaticAssets();
#endif
var razorBuilder = app.MapRazorComponents<App>().AddInteractiveServerRenderMode();
foreach (var mod in SptWebMods)
{
foreach (var assembly in mod.Assemblies)
{
razorBuilder.AddAdditionalAssemblies(assembly);
}
var modAssembly = mod.ModMetadata.GetType().Assembly;
var location = Path.GetDirectoryName(modAssembly.Location);
if (!string.IsNullOrEmpty(location) && Directory.Exists(Path.Combine(location, "wwwroot")))
{
var modAssemblyName = modAssembly.GetName().Name;
logger.LogDebug(
"Mod {modName} has a wwwroot, mapping to /{modAssemblyName}/",
mod.ModMetadata.Name,
modAssembly.GetName().Name
);
app.UseStaticFiles(
new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(Path.Combine(location, "wwwroot")),
RequestPath = $"/{modAssembly.GetName().Name}",
}
);
}
}
app.MapControllers();
}
}
@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Import Project="..\..\Build.props" />
<PropertyGroup>
<PackageId>SPTarkov.Server.Web</PackageId>
<Authors>Single Player Tarkov</Authors>
<Description>Common shared library for the Single Player Tarkov projects.</Description>
<Copyright>Copyright (c) Single Player Tarkov 2025</Copyright>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageProjectUrl>https://sp-tarkov.com</PackageProjectUrl>
<RepositoryUrl>https://github.com/sp-tarkov/server-csharp</RepositoryUrl>
<ImplicitUsings>enable</ImplicitUsings>
<OutputType>Library</OutputType>
<IsPackable>true</IsPackable>
<NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<!-- Has to be enabled for SPTarkov.Server.Web to function properly-->
<StaticWebAssetsEnabled>true</StaticWebAssetsEnabled>
<StaticWebAssetBasePath>SPTarkov.Server.Web</StaticWebAssetBasePath>
</PropertyGroup>
<ItemGroup>
<None Include="..\..\LICENSE" Pack="true" Visible="false" PackagePath="" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="MudBlazor" Version="8.12.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SPTarkov.Server.Core\SPTarkov.Server.Core.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7532edb85d6c446c38d9e7e7a7e2f31544ba62ab502381396c86bf9934cd4db8
size 61186672
+9 -3
View File
@@ -1,5 +1,6 @@
using System.Net; using System.Net;
using System.Net.Sockets; using System.Net.Sockets;
using System.Reflection;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Security.Authentication; using System.Security.Authentication;
using System.Text; using System.Text;
@@ -20,6 +21,7 @@ using SPTarkov.Server.Core.Utils.Logger;
using SPTarkov.Server.Logger; using SPTarkov.Server.Logger;
using SPTarkov.Server.Modding; using SPTarkov.Server.Modding;
using SPTarkov.Server.Services; using SPTarkov.Server.Services;
using SPTarkov.Server.Web;
namespace SPTarkov.Server; namespace SPTarkov.Server;
@@ -41,7 +43,7 @@ public static class Program
ProgramStatics.Initialize(); ProgramStatics.Initialize();
// Create web builder and logger // Create web builder and logger
var builder = CreateNewHostBuilder(args); var builder = CreateNewHostBuilder();
var diHandler = new DependencyInjectionHandler(builder.Services); var diHandler = new DependencyInjectionHandler(builder.Services);
// register SPT components // register SPT components
@@ -64,6 +66,8 @@ public static class Program
} }
diHandler.InjectAll(); diHandler.InjectAll();
builder.InitializeSptBlazor(loadedMods);
builder.Services.AddSingleton(builder); builder.Services.AddSingleton(builder);
builder.Services.AddSingleton<IReadOnlyList<SptMod>>(loadedMods); builder.Services.AddSingleton<IReadOnlyList<SptMod>>(loadedMods);
// Configure Kestrel options // Configure Kestrel options
@@ -125,6 +129,8 @@ public static class Program
await context.RequestServices.GetRequiredService<HttpServer>().HandleRequest(context, next); await context.RequestServices.GetRequiredService<HttpServer>().HandleRequest(context, next);
} }
); );
app.UseSptBlazor();
} }
private static void ConfigureKestrel(WebApplicationBuilder builder) private static void ConfigureKestrel(WebApplicationBuilder builder)
@@ -167,9 +173,9 @@ public static class Program
); );
} }
private static WebApplicationBuilder CreateNewHostBuilder(string[]? args = null) private static WebApplicationBuilder CreateNewHostBuilder()
{ {
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(new WebApplicationOptions { WebRootPath = "./SPT_Data/wwwroot" });
builder.Logging.ClearProviders(); builder.Logging.ClearProviders();
builder.Configuration.SetBasePath(Directory.GetCurrentDirectory()); builder.Configuration.SetBasePath(Directory.GetCurrentDirectory());
builder.Host.UseSptLogger(); builder.Host.UseSptLogger();
+4 -1
View File
@@ -14,8 +14,10 @@
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<IsPackable>true</IsPackable> <IsPackable>true</IsPackable>
<AssemblyName>SPT.Server</AssemblyName> <AssemblyName>SPT.Server</AssemblyName>
<StaticWebAssetsEnabled>false</StaticWebAssetsEnabled>
<NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile> <NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile>
<!-- Has to be enabled for SPTarkov.Server.Web to function properly-->
<StaticWebAssetsEnabled>true</StaticWebAssetsEnabled>
<StaticWebAssetBasePath>../SPT_Data/wwwroot/</StaticWebAssetBasePath>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(RuntimeIdentifier)' == 'win-x64'"> <PropertyGroup Condition="'$(RuntimeIdentifier)' == 'win-x64'">
<ApplicationIcon>..\Libraries\SPTarkov.Server.Assets\SPT_Data\images\icon.ico</ApplicationIcon> <ApplicationIcon>..\Libraries\SPTarkov.Server.Assets\SPT_Data\images\icon.ico</ApplicationIcon>
@@ -27,6 +29,7 @@
<ProjectReference Include="..\Libraries\SPTarkov.Reflection\SPTarkov.Reflection.csproj" /> <ProjectReference Include="..\Libraries\SPTarkov.Reflection\SPTarkov.Reflection.csproj" />
<ProjectReference Include="..\Libraries\SPTarkov.Server.Core\SPTarkov.Server.Core.csproj" /> <ProjectReference Include="..\Libraries\SPTarkov.Server.Core\SPTarkov.Server.Core.csproj" />
<ProjectReference Include="..\Libraries\SPTarkov.Server.Assets\SPTarkov.Server.Assets.csproj" /> <ProjectReference Include="..\Libraries\SPTarkov.Server.Assets\SPTarkov.Server.Assets.csproj" />
<ProjectReference Include="..\Libraries\SPTarkov.Server.Web\SPTarkov.Server.Web.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.9" /> <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.9" />
@@ -0,0 +1,9 @@
@page "/test/page"
<h3>TestModPage</h3>
<img src="/TestMod/chomp.jpg" alt="Chomp!" />
@code {
}
@@ -0,0 +1,12 @@
using Microsoft.AspNetCore.Mvc;
namespace TestMod.Controllers;
public class TestController : Controller
{
[HttpGet("/test/ping")]
public IActionResult Ping()
{
return Content("Pong from MVC!");
}
}
+4 -1
View File
@@ -2,11 +2,12 @@
using SPTarkov.Server.Core.DI; using SPTarkov.Server.Core.DI;
using SPTarkov.Server.Core.Models.Spt.Mod; using SPTarkov.Server.Core.Models.Spt.Mod;
using SPTarkov.Server.Core.Models.Utils; using SPTarkov.Server.Core.Models.Utils;
using SPTarkov.Server.Web;
using Version = SemanticVersioning.Version; using Version = SemanticVersioning.Version;
namespace TestMod; namespace TestMod;
public record TestModMetadata : AbstractModMetadata public record TestModMetadata : AbstractModMetadata, IModWebMetadata
{ {
public override string ModGuid { get; init; } = "com.sp-tarkov.test-mod"; public override string ModGuid { get; init; } = "com.sp-tarkov.test-mod";
public override string Name { get; init; } = "test-mod"; public override string Name { get; init; } = "test-mod";
@@ -26,6 +27,8 @@ public class TestMod(ISptLogger<TestMod> logger) : IOnLoad
{ {
public Task OnLoad() public Task OnLoad()
{ {
logger.Info("Test mod loaded!");
return Task.CompletedTask; return Task.CompletedTask;
} }
} }
+15 -7
View File
@@ -1,8 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk.Web">
<Import Project="..\..\Build.props" /> <Import Project="..\..\Build.props" />
<PropertyGroup> <PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<AssemblyName>TestMod2</AssemblyName> <AssemblyName>TestMod</AssemblyName>
<OutputType>Library</OutputType>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\Libraries\SPTarkov.Common\SPTarkov.Common.csproj" /> <ProjectReference Include="..\..\Libraries\SPTarkov.Common\SPTarkov.Common.csproj" />
@@ -10,20 +11,27 @@
<ProjectReference Include="..\..\Libraries\SPTarkov.Reflection\SPTarkov.Reflection.csproj" /> <ProjectReference Include="..\..\Libraries\SPTarkov.Reflection\SPTarkov.Reflection.csproj" />
<ProjectReference Include="..\..\Libraries\SPTarkov.Server.Assets\SPTarkov.Server.Assets.csproj" /> <ProjectReference Include="..\..\Libraries\SPTarkov.Server.Assets\SPTarkov.Server.Assets.csproj" />
<ProjectReference Include="..\..\Libraries\SPTarkov.Server.Core\SPTarkov.Server.Core.csproj" /> <ProjectReference Include="..\..\Libraries\SPTarkov.Server.Core\SPTarkov.Server.Core.csproj" />
<ProjectReference Include="..\..\Libraries\SPTarkov.Server.Web\SPTarkov.Server.Web.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="wwwroot\" />
</ItemGroup> </ItemGroup>
<Target Name="CopyToServer" AfterTargets="PostBuildEvent"> <Target Name="CopyToServer" AfterTargets="PostBuildEvent">
<ItemGroup> <ItemGroup>
<OutputDLL Include="$(ProjectDir)$(OutDir)$(TargetName).dll" /> <OutputDLL Include="$(ProjectDir)$(OutDir)$(TargetName).dll" />
<Resources Include="$(ProjectDir)Resources\**\*.*" /> <Resources Include="$(ProjectDir)Resources\**\*.*" />
<WebAssets Include="$(ProjectDir)wwwroot\**\*.*" />
</ItemGroup> </ItemGroup>
<!-- Copies the output dll --> <!-- Copies the output dll -->
<Copy <Copy SourceFiles="@(OutputDLL);" DestinationFolder="$(SolutionDir)\SPTarkov.Server\bin\$(Configuration)\net9.0\user\mods\TestMod" />
SourceFiles="@(OutputDLL);"
DestinationFolder="$(SolutionDir)\SPTarkov.Server\bin\$(Configuration)\net9.0\user\mods\$(TargetName)"
/>
<Copy <Copy
SourceFiles="@(Resources);" SourceFiles="@(Resources);"
DestinationFolder="$(SolutionDir)\SPTarkov.Server\bin\$(Configuration)\net9.0\user\mods\$(TargetName)\Resources\%(RecursiveDir)" DestinationFolder="$(SolutionDir)\SPTarkov.Server\bin\$(Configuration)\net9.0\user\mods\TestMod\Resources\%(RecursiveDir)"
/>
<!-- Copy the wwwroot folder -->
<Copy
SourceFiles="@(WebAssets)"
DestinationFolder="$(SolutionDir)\SPTarkov.Server\bin\$(Configuration)\net9.0\user\mods\TestMod\wwwroot\%(RecursiveDir)"
/> />
</Target> </Target>
</Project> </Project>
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

+11 -1
View File
@@ -1,7 +1,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17 # Visual Studio Version 17
VisualStudioVersion = 17.12.35527.113 d17.12 VisualStudioVersion = 17.12.35527.113
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SPTarkov.Server", "SPTarkov.Server\SPTarkov.Server.csproj", "{1F5ED9C6-8B1F-4776-85AB-B387CBBC5557}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SPTarkov.Server", "SPTarkov.Server\SPTarkov.Server.csproj", "{1F5ED9C6-8B1F-4776-85AB-B387CBBC5557}"
EndProject EndProject
@@ -43,6 +43,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestMod", "Testing\TestMod\
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ceciler.JsonExtensionData", "Patches\Ceciler.JsonExtensionData\Ceciler.JsonExtensionData.csproj", "{5D09182A-B0B3-406C-AE88-EE0929F9260C}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ceciler.JsonExtensionData", "Patches\Ceciler.JsonExtensionData\Ceciler.JsonExtensionData.csproj", "{5D09182A-B0B3-406C-AE88-EE0929F9260C}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SPTarkov.Server.Web", "Libraries\SPTarkov.Server.Web\SPTarkov.Server.Web.csproj", "{BB1EB56E-9D40-8497-5A6D-B2E35E83FA89}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -105,6 +107,10 @@ Global
{5D09182A-B0B3-406C-AE88-EE0929F9260C}.Debug|Any CPU.Build.0 = Debug|Any CPU {5D09182A-B0B3-406C-AE88-EE0929F9260C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5D09182A-B0B3-406C-AE88-EE0929F9260C}.Release|Any CPU.ActiveCfg = Release|Any CPU {5D09182A-B0B3-406C-AE88-EE0929F9260C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5D09182A-B0B3-406C-AE88-EE0929F9260C}.Release|Any CPU.Build.0 = Release|Any CPU {5D09182A-B0B3-406C-AE88-EE0929F9260C}.Release|Any CPU.Build.0 = Release|Any CPU
{BB1EB56E-9D40-8497-5A6D-B2E35E83FA89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BB1EB56E-9D40-8497-5A6D-B2E35E83FA89}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BB1EB56E-9D40-8497-5A6D-B2E35E83FA89}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BB1EB56E-9D40-8497-5A6D-B2E35E83FA89}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@@ -123,5 +129,9 @@ Global
{28B90486-1436-4CD7-88D0-122B6963AB58} = {07B50C44-6D38-474E-87AF-68672D241EEB} {28B90486-1436-4CD7-88D0-122B6963AB58} = {07B50C44-6D38-474E-87AF-68672D241EEB}
{755E473C-14F2-40BC-9377-2FAB11CA91DC} = {07B50C44-6D38-474E-87AF-68672D241EEB} {755E473C-14F2-40BC-9377-2FAB11CA91DC} = {07B50C44-6D38-474E-87AF-68672D241EEB}
{5D09182A-B0B3-406C-AE88-EE0929F9260C} = {9E41CD5A-271C-4294-AAF9-8EB379311416} {5D09182A-B0B3-406C-AE88-EE0929F9260C} = {9E41CD5A-271C-4294-AAF9-8EB379311416}
{BB1EB56E-9D40-8497-5A6D-B2E35E83FA89} = {F084DDFD-89F3-44F9-89C3-5CA11F4CDEEF}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {6F730FC0-94A8-40B8-8E0E-5D6558E8422A}
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal