-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
✨ Chat log/WR chat log for any demo #5
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
using TempusHub.API.Features.Demos.Services; | ||
|
||
namespace TempusHub.API.Features.Demos; | ||
|
||
public class DemoModule : IModule | ||
{ | ||
public static void ConfigureServices(IServiceCollection services) | ||
{ | ||
services.AddScoped<TempusDemoService>(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
using System.IO.Compression; | ||
using System.Text.RegularExpressions; | ||
using Microsoft.AspNetCore.Mvc; | ||
using TempusApi; | ||
using TempusHub.API.Features.Demos.Services; | ||
using TempusHub.API.Features.Reports.Queries.GetWorldRecordHistory; | ||
|
||
namespace TempusHub.API.Features.Demos.Queries.GetChatLogsQuery; | ||
|
||
public sealed class GetChatLogsQueryEndpoint : IEndpoint | ||
{ | ||
public static void MapEndpoint(IEndpointRouteBuilder endpoints) | ||
{ | ||
endpoints.MapGetWithOpenApi<string[]>("/chat-log", HandleAsync) | ||
.WithTags("Demos"); | ||
} | ||
|
||
public static async Task<IResult> HandleAsync([FromQuery] long demoId, TempusDemoService tempusDemoService, CancellationToken cancellationToken) | ||
{ | ||
var stvData = await tempusDemoService.ExtractDemoAsync(demoId, cancellationToken); | ||
|
||
var messages = stvData.Chat | ||
.Where(x => !string.IsNullOrWhiteSpace(x.Text)) | ||
.Select(x => x.Text) | ||
.ToList(); | ||
|
||
return Results.Ok(messages); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
using System.Text.RegularExpressions; | ||
using System.Web; | ||
using Microsoft.AspNetCore.Mvc; | ||
using SteamWebAPI2.Interfaces; | ||
using SteamWebAPI2.Models; | ||
using SteamWebAPI2.Utilities; | ||
using TempusApi; | ||
using TempusApi.Enums; | ||
using TempusHub.API.Features.Demos.Services; | ||
using TempusHub.API.Features.Reports.Queries.GetWorldRecordHistory; | ||
|
||
namespace TempusHub.API.Features.Demos.Queries.GetWorldRecordChatLogsQuery; | ||
|
||
public class GetWorldRecordChatLogsQueryEndpoint : IEndpoint | ||
{ | ||
public static void MapEndpoint(IEndpointRouteBuilder endpoints) | ||
{ | ||
endpoints.MapGetWithOpenApi<WorldRecordHistoryResponse[]>("/chat-log/world-records", HandleAsync) | ||
.WithTags("Demos"); | ||
} | ||
|
||
public static async Task<IResult> HandleAsync([FromQuery] long demoId, [FromQuery] Class? @class, TempusDemoService tempusDemoService, SteamWebInterfaceFactory webInterfaceFactory, ITempusClient tempusClient, CancellationToken cancellationToken) | ||
{ | ||
var stvData = await tempusDemoService.ExtractDemoAsync(demoId, cancellationToken); | ||
|
||
var messages = stvData.Chat | ||
.Where(x => !string.IsNullOrWhiteSpace(x.Text)) | ||
.Select(x => x.Text) | ||
.ToList(); | ||
|
||
const string Pattern = @"^Tempus \| \(([^)]+)\) (.*?) beat the map record: (\d{2}:\d{2}\.\d{2}) \(WR -(\d{2}:\d{2}\.\d{2})\) \| (\d{2}:\d{2}\.\d{2}) improvement!$"; | ||
|
||
var output = new List<WorldRecordChatLogResponse>(); | ||
|
||
foreach (var message in messages) | ||
{ | ||
var match = Regex.Match(message, Pattern); | ||
if (!match.Success) | ||
{ | ||
continue; | ||
} | ||
|
||
var detectedClass = match.Groups[1].Value switch | ||
{ | ||
"Solly" => Class.Soldier, | ||
"Demo" => Class.Demoman | ||
}; | ||
var player = match.Groups[2].Value; | ||
var time = match.Groups[3].Value; | ||
var wrSplit = match.Groups[4].Value; | ||
var prSplit = match.Groups[5].Value; | ||
|
||
var steamId = stvData.Users.FirstOrDefault(x => x.Value.Name == player).Value.SteamId; | ||
|
||
var steamInterface = webInterfaceFactory.CreateSteamWebInterface<SteamUser>(new HttpClient()); | ||
var playerSummaryResponse = await steamInterface.GetPlayerSummaryAsync(UsteamidToCommid(steamId)); | ||
|
||
var playerInfo = await tempusClient.GetSearchResultAsync(HttpUtility.UrlEncode(playerSummaryResponse.Data.Nickname), cancellationToken); | ||
output.Add(new WorldRecordChatLogResponse(detectedClass, player, time, wrSplit, prSplit, steamId, playerInfo.Players.FirstOrDefault(), new SteamUserInfo(playerSummaryResponse.Data.Nickname, playerSummaryResponse.Data.SteamId, playerSummaryResponse.Data.ProfileUrl, playerSummaryResponse.Data.AvatarFullUrl))); | ||
} | ||
|
||
return Results.Ok(output); | ||
} | ||
|
||
private const ulong steamid64ident = 76561197960265728; | ||
|
||
public static ulong UsteamidToCommid(string usteamid) | ||
{ | ||
usteamid = usteamid.Replace("[", "").Replace("]", ""); | ||
|
||
string[] usteamidSplit = usteamid.Split(':'); | ||
ulong commid = ulong.Parse(usteamidSplit[2]) + steamid64ident; | ||
|
||
return commid; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
using TempusApi.Enums; | ||
using TempusApi.Models; | ||
|
||
namespace TempusHub.API.Features.Demos.Queries.GetWorldRecordChatLogsQuery; | ||
|
||
public record WorldRecordChatLogResponse( | ||
Class Class, | ||
string PlayerName, | ||
string RunDuration, | ||
string WorldRecordSplit, | ||
string PersonalRecordSplit, | ||
string SteamId, | ||
ServerPlayerModel? PlayerInfo, | ||
SteamUserInfo UserInfo); | ||
|
||
public record SteamUserInfo(string Name, ulong SteamId, string ProfileUrl, string AvatarUrl); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
using System.Diagnostics; | ||
using System.Text.Json; | ||
|
||
namespace TempusHub.API.Features.Demos.Services; | ||
|
||
public static class StvParser | ||
{ | ||
public static StvParserResponse ExtractStvData(string fileName) | ||
{ | ||
var processStartInfo = new ProcessStartInfo | ||
{ | ||
FileName = "parse_demo.exe", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. move to config. locally this will be an exe, hosted this will probably be linux, not an EXE and different dir maybe. |
||
Arguments = fileName, | ||
RedirectStandardOutput = true, | ||
UseShellExecute = false | ||
}; | ||
var process = Process.Start(processStartInfo); | ||
var output = process.StandardOutput.ReadToEnd(); | ||
process.WaitForExit(); | ||
|
||
if (output is null) | ||
{ | ||
throw new InvalidOperationException("STV was invalid, parsing failed."); | ||
} | ||
|
||
return JsonSerializer.Deserialize<StvParserResponse>(output) ?? throw new InvalidOperationException("STV was invalid, parsing failed. Error: " + output); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
using System.Text.Json.Serialization; | ||
|
||
namespace TempusHub.API.Features.Demos.Services; | ||
|
||
public record User( | ||
[property: JsonPropertyName("classes")] | ||
Classes Classes, | ||
[property: JsonPropertyName("name")] string Name, | ||
[property: JsonPropertyName("userId")] int? UserId, | ||
[property: JsonPropertyName("steamId")] | ||
string SteamId, | ||
[property: JsonPropertyName("team")] string Team | ||
); | ||
|
||
public record Chat( | ||
[property: JsonPropertyName("kind")] string Kind, | ||
[property: JsonPropertyName("from")] string From, | ||
[property: JsonPropertyName("text")] string Text, | ||
[property: JsonPropertyName("tick")] int? Tick | ||
); | ||
|
||
public record Classes( | ||
[property: JsonPropertyName("0")] int? _0, | ||
[property: JsonPropertyName("3")] int? _3, | ||
[property: JsonPropertyName("9")] int? _9, | ||
[property: JsonPropertyName("4")] int? _4, | ||
[property: JsonPropertyName("1")] int? _1, | ||
[property: JsonPropertyName("7")] int? _7 | ||
); | ||
Comment on lines
+22
to
+29
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. to a dictionary |
||
|
||
public record Death( | ||
[property: JsonPropertyName("weapon")] string Weapon, | ||
[property: JsonPropertyName("victim")] int? Victim, | ||
[property: JsonPropertyName("assister")] | ||
object Assister, | ||
[property: JsonPropertyName("killer")] int? Killer, | ||
[property: JsonPropertyName("tick")] int? Tick | ||
); | ||
|
||
public record Header( | ||
[property: JsonPropertyName("demo_type")] | ||
string DemoType, | ||
[property: JsonPropertyName("version")] | ||
int? Version, | ||
[property: JsonPropertyName("protocol")] | ||
int? Protocol, | ||
[property: JsonPropertyName("server")] string Server, | ||
[property: JsonPropertyName("nick")] string Nick, | ||
[property: JsonPropertyName("map")] string Map, | ||
[property: JsonPropertyName("game")] string Game, | ||
[property: JsonPropertyName("duration")] | ||
double? Duration, | ||
[property: JsonPropertyName("ticks")] int? Ticks, | ||
[property: JsonPropertyName("frames")] int? Frames, | ||
[property: JsonPropertyName("signon")] int? Signon | ||
); | ||
|
||
public record StvParserResponse( | ||
[property: JsonPropertyName("header")] Header Header, | ||
[property: JsonPropertyName("chat")] IReadOnlyList<Chat> Chat, | ||
[property: JsonPropertyName("users")] Dictionary<string, User> Users, | ||
[property: JsonPropertyName("deaths")] IReadOnlyList<Death> Deaths, | ||
[property: JsonPropertyName("rounds")] IReadOnlyList<object> Rounds, | ||
[property: JsonPropertyName("startTick")] | ||
int? StartTick, | ||
[property: JsonPropertyName("intervalPerTick")] | ||
double? IntervalPerTick, | ||
[property: JsonPropertyName("pauses")] IReadOnlyList<object> Pauses | ||
); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
using System.IO.Compression; | ||
using TempusApi; | ||
|
||
namespace TempusHub.API.Features.Demos.Services; | ||
|
||
public class TempusDemoService(ITempusClient tempusClient, HttpClient httpClient) | ||
{ | ||
public async Task<StvParserResponse> ExtractDemoAsync(long demoId, CancellationToken cancellationToken) | ||
{ | ||
var demo = await tempusClient.GetDemoInfoAsync(demoId, cancellationToken); | ||
var url = demo.Overview.Url; | ||
|
||
var downloadStream = httpClient.GetStreamAsync(url, cancellationToken); | ||
|
||
var zipFilePath = Path.Join(Environment.CurrentDirectory, "demos", demo.Overview.FileName + ".zip"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. config? |
||
Directory.CreateDirectory(Path.GetDirectoryName(zipFilePath) ?? string.Empty); | ||
await using (var fileStream = File.Create(zipFilePath)) | ||
{ | ||
await downloadStream.Result.CopyToAsync(fileStream, cancellationToken); | ||
|
||
await fileStream.FlushAsync(cancellationToken); | ||
} | ||
|
||
var filePath = zipFilePath.Replace(".zip", ".dem"); | ||
|
||
// Its a zip file, extract the underlying .dem | ||
using (var archive = ZipFile.OpenRead(zipFilePath)) | ||
{ | ||
var entry = archive.Entries.FirstOrDefault(e => e.FullName.EndsWith(".dem")); | ||
if (entry is null) | ||
{ | ||
throw new InvalidOperationException("Demo was not a valid zip file"); | ||
} | ||
|
||
|
||
entry.ExtractToFile(filePath, true); | ||
} | ||
|
||
var output = StvParser.ExtractStvData(filePath); | ||
|
||
File.Delete(zipFilePath); | ||
File.Delete(filePath); | ||
|
||
return output; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use a common library?