Skip to content
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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions API/src/TempusHub.API/Features/Demos/DemoModule.cs
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;
}
Comment on lines +65 to +75
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use a common library?

}
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);
28 changes: 28 additions & 0 deletions API/src/TempusHub.API/Features/Demos/Services/StvParser.cs
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",
Copy link
Owner Author

Choose a reason for hiding this comment

The 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);
}
}
69 changes: 69 additions & 0 deletions API/src/TempusHub.API/Features/Demos/Services/StvParserResponse.cs
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
Copy link
Owner Author

Choose a reason for hiding this comment

The 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
);
46 changes: 46 additions & 0 deletions API/src/TempusHub.API/Features/Demos/Services/TempusDemoService.cs
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");
Copy link
Owner Author

Choose a reason for hiding this comment

The 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;
}
}
4 changes: 4 additions & 0 deletions API/src/TempusHub.API/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.Reflection;
using System.Text.Json.Serialization;
using SteamWebAPI2.Utilities;
using TempusApi;
using TempusHub.API.Common;
using TempusHub.API.Kernel;
Expand All @@ -11,6 +13,8 @@
builder.Services.AddSingleton<ITempusClient, TempusClient>();
builder.Services.AddHttpClient<TempusClient>();

builder.Services.AddSingleton(new SteamWebInterfaceFactory(builder.Configuration["SteamApiKey"]));

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

Expand Down
6 changes: 2 additions & 4 deletions API/src/TempusHub.API/TempusHub.API.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>3d17fc89-33c9-4f41-b60d-e8773edb4683</UserSecretsId>
</PropertyGroup>

<ItemGroup>
Expand All @@ -14,12 +15,9 @@
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.0"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.0" />
<PackageReference Include="SteamWebAPI2" Version="4.4.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0"/>
<PackageReference Include="TempusApi" Version="3.0.6" />
</ItemGroup>

<ItemGroup>
<Folder Include="Features\" />
</ItemGroup>

</Project>