Skip to content

Commit

Permalink
>:3
Browse files Browse the repository at this point in the history
  • Loading branch information
Simyon264 committed Aug 30, 2024
1 parent d4346ff commit efb6871
Show file tree
Hide file tree
Showing 5 changed files with 280 additions and 29 deletions.
29 changes: 22 additions & 7 deletions ReplayBrowser/Controllers/AccountController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -230,17 +230,24 @@ [FromQuery] string guid
var historyEntry = archive.CreateEntry("history.json");
using (var entryStream = historyEntry.Open())
{
await JsonSerializer.SerializeAsync(entryStream, user.History);
await JsonSerializer.SerializeAsync(entryStream, user.History, new JsonSerializerOptions()
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
ReferenceHandler = ReferenceHandler.IgnoreCycles,
WriteIndented = true
});
}

user.History = [];

var baseEntry = archive.CreateEntry("user.json");
using (var entryStream = baseEntry.Open())
{
await JsonSerializer.SerializeAsync(entryStream, user, new JsonSerializerOptions
await JsonSerializer.SerializeAsync(entryStream, user, new JsonSerializerOptions()
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
ReferenceHandler = ReferenceHandler.IgnoreCycles,
WriteIndented = true
});
}
}
Expand All @@ -256,10 +263,11 @@ [FromQuery] string guid
var replayEntry = archive.CreateEntry($"replay-{replay.Id}.json");
using (var entryStream = replayEntry.Open())
{
await JsonSerializer.SerializeAsync(entryStream, replay, new JsonSerializerOptions
await JsonSerializer.SerializeAsync(entryStream, replay, new JsonSerializerOptions()
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
ReferenceHandler = ReferenceHandler.IgnoreCycles
ReferenceHandler = ReferenceHandler.IgnoreCycles,
WriteIndented = true
});
}
}
Expand Down Expand Up @@ -424,7 +432,12 @@ public async Task<IActionResult> DownloadAccount()
var historyEntry = archive.CreateEntry("history.json");
using (var entryStream = historyEntry.Open())
{
await JsonSerializer.SerializeAsync(entryStream, user.History);
await JsonSerializer.SerializeAsync(entryStream, user.History, new JsonSerializerOptions()
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
ReferenceHandler = ReferenceHandler.IgnoreCycles,
WriteIndented = true
});
}

user.History = [];
Expand All @@ -434,7 +447,9 @@ public async Task<IActionResult> DownloadAccount()
{
await JsonSerializer.SerializeAsync(entryStream, user, new JsonSerializerOptions
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
ReferenceHandler = ReferenceHandler.IgnoreCycles,
WriteIndented = true
});
}
}
Expand Down
1 change: 1 addition & 0 deletions ReplayBrowser/Pages/Shared/Layout/App.razor
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<script src="jquery-3.7.1.min.js"></script>
<script src="bootstrap/js/bootstrap.bundle.js"></script>
<script src="bootstrap/js/bootstrap-autocomplete.min.js"></script>
<script src="vis-timeline-graph2d.min.js"></script>
<link rel="stylesheet" href="app.css"/>
<link rel="icon" type="image/png" href="favicon.png"/>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css">
Expand Down
165 changes: 152 additions & 13 deletions ReplayBrowser/Pages/Shared/Layout/MainLayout.razor
Original file line number Diff line number Diff line change
Expand Up @@ -174,51 +174,190 @@
});
}
async function loadTimeline(timelineElement, replayId, detailsContent) {
let response = await fetch(`/api/Replay/${replayId}`);
let data = await response.json();
if (data.Events == null || data.Events.length == 0) {
timelineElement.innerHTML = "<p class='card-text text-danger'>No events available.</p>";
return;
}
const items = data.Events.map((event, index) => {
const date = new Date(data.Date);
date.setSeconds(date.getSeconds() + event.Time);
// Deep loop through every property of the event and replace < and > with &lt; and &gt;
// This is to prevent XSS attacks
function escapeHtml(obj) {
if (obj === null || obj === undefined) {
return obj;
}
if (typeof obj === 'string') {
return obj.replace(/</g, '&lt;').replace(/>/g, '&gt;');
} else if (typeof obj === 'object') {
for (const [key, value] of Object.entries(obj)) {
obj[key] = escapeHtml(value);
}
}
return obj;
}
event = escapeHtml(event);
let content = "";
switch (event.EventTypeString) {
case "PlayerJoin":
content = `<p><strong>${event.Target.PlayerOocName}</strong> joined the round</p>`;
break;
case "PlayerLeave":
content = `<p><strong>${event.Target.PlayerOocName}</strong> left the round</p>`;
break;
case "GameRuleStarted":
content = `<p><strong>${event.Target}</strong> started</p>`;
break
case "GameRuleEnded":
content = `<p><strong>${event.Target}</strong> ended</p>`;
break;
case "RoundEnded":
content = `<p>Round ended</p>`;
break;
case "ChatMessageSent":
switch (event.Type) {
case "Local":
content = `<p><strong>${event.Sender.PlayerIcName} (${event.Sender.PlayerOocName})</strong> said: ${event.Message}</p>`;
break;
case "Emotes":
content = `<p><strong>${event.Sender.PlayerIcName} (${event.Sender.PlayerOocName})</strong> <i>${event.Message}</i></p>`;
break;
case "Dead": // color purple
content = `<p style="color: rebeccapurple"><strong>${event.Sender.PlayerIcName} (${event.Sender.PlayerOocName})</strong> said: ${event.Message}</p>`;
break;
default:
content = `<p><strong>${event.Sender.PlayerIcName} (${event.Sender.PlayerOocName})</strong> said: ${event.Message}</p>`;
break;
}
break;
case "AlertLevelChanged":
content = `<p>Alert level changed to <strong>${event.AlertLevel}</strong></p>`;
break;
case "MobStateChanged":
if (event.OldStateString == "Alive" && event.NewStateString == "Dead") {
content = `<p><strong>${event.Target.PlayerIcName} (${event.Target.PlayerOocName})</strong> died</p>`;
} else if (event.OldStateString == "Alive" && event.NewStateString == "Crit") {
content = `<p><strong>${event.Target.PlayerIcName} (${event.Target.PlayerOocName})</strong> went into critical condition</p>`;
} else if (event.OldStateString == "Crit" && event.NewStateString == "Alive") {
content = `<p><strong>${event.Target.PlayerIcName} (${event.Target.PlayerOocName})</strong> was revived.</p>`;
} else if (event.OldStateString == "Crit" && event.NewStateString == "Dead") {
content = `<p><strong>${event.Target.PlayerIcName} (${event.Target.PlayerOocName})</strong> died</p>`;
} else if (event.OldStateString == "Dead" && event.NewStateString == "Crit") {
content = `<p><strong>${event.Target.PlayerIcName} (${event.Target.PlayerOocName})</strong> was revived</p>`;
} else {
content = `<p><strong>${event.Target.PlayerIcName} (${event.Target.PlayerOocName})</strong> changed state from <strong>${event.OldStateString}</strong> to <strong>${event.NewStateString}</strong></p>`;
}
break;
default:
content = event.EventTypeString;
break;
}
return {
id: index + 1,
content: content,
start: date,
eventDetails: event,
};
});
const dataset = new vis.DataSet(items);
const options = {
width: '100%',
height: '800px',
margin: {
item: 20
},
editable: false,
stack: true,
zoomMin: 1000, // milliseconds
zoomMax: 1000 * 60 * 60, // milliseconds
showCurrentTime: false,
showMajorLabels: true,
showMinorLabels: true,
xss: {disabled: true}
};
let timeline = new vis.Timeline(timelineElement, items, options);
timeline.on('select', function (properties) {
const selectedItem = dataset.get(properties.items[0]);
if (selectedItem) {
displayEventDetails(selectedItem.eventDetails);
}
});
function displayEventDetails(event) {
detailsContent.innerHTML = '';
for (const [key, value] of Object.entries(event)) {
if (typeof value === 'object' && value !== null) {
detailsContent.innerHTML += `<strong>${key}:</strong><pre>${JSON.stringify(value, null, 2)}</pre><br>`;
} else {
detailsContent.innerHTML += `<strong>${key}:</strong> ${value}<br>`;
}
}
}
}
// Once the modal is opened, we need to rerequest the round end players if they are null
// This is because the modal doesn't load the data until it is opened
async function loadDetails(playersElemn, endTextElement, replayId) {
let response = await fetch(`/api/Replay/${replayId}`);
let data = await response.json();
let playerList = "";
if (data.roundParticipants == null || data.roundParticipants.length == 0) {
if (data.RoundParticipants == null || data.RoundParticipants.length == 0) {
playersElemn.innerHTML = "<p class='card-text text-danger'>Replay is incomplete. No players available.</p>";
} else {
let players = data.roundParticipants.flatMap(
let players = data.RoundParticipants.flatMap(
pc => pc.players.map(
pl => ({
...pl,
playerGuid: pc.playerGuid,
PlayerGuid: pc.PlayerGuid,
username: pc.username
})
)
)
// Sort the players so that antags are at the top
players.sort((a, b) => {
if (a.antagPrototypes.length > 0 && b.antagPrototypes.length == 0) {
if (a.AntagPrototypes.length > 0 && b.AntagPrototypes.length == 0) {
return -1;
} else if (a.antagPrototypes.length == 0 && b.antagPrototypes.length > 0) {
} else if (a.AntagPrototypes.length == 0 && b.AntagPrototypes.length > 0) {
return 1;
}
return 0;
});
playersElemn.innerHTML = players.map(player => {
let job = "Unknown";
if (player.jobPrototypes.length > 0) {
job = player.jobPrototypes[0];
if (player.JobPrototypes.length > 0) {
job = player.JobPrototypes[0];
}
let playerText = `<a href="/player/${player.playerGuid}"><span style="color: gray">${player.username}</span></a> was <bold>${player.playerIcName}</bold> playing role of <span style="color: orange"><bold>${job}</bold></span>`;
if (player.antagPrototypes.length > 0) {
playerText = `<a href="/player/${player.playerGuid}"><span style="color: red">${player.username}</span></a> was <span style="color:red"><bold>${player.playerIcName}</bold></span> playing role of <span style="color: orange"><bold>${job}</bold></span>`;
let playerText = `<a href="/player/${player.PlayerGuid}"><span style="color: gray">${player.Username}</span></a> was <bold>${player.PlayerIcName}</bold> playing role of <span style="color: orange"><bold>${job}</bold></span>`;
if (player.AntagPrototypes.length > 0) {
playerText = `<a href="/player/${player.PlayerGuid}"><span style="color: red">${player.Username}</span></a> was <span style="color:red"><bold>${player.PlayerIcName}</bold></span> playing role of <span style="color: orange"><bold>${job}</bold></span>`;
}
// Need to show the guid as well
playerText += `<br><span style="color: gray;font-size: x-small;"> ${player.playerGuid}</span>`;
playerText += `<br><span style="color: gray;font-size: x-small;"> ${player.PlayerGuid}</span>`;
return playerText;
}).join("<br>\n");
}
if (!data.roundEndText || data.roundEndText.trim() == "") {
if (!data.RoundEndText || data.RoundEndText.trim() == "") {
endTextElement.innerHTML = "No round end text available.";
} else {
const colorTagPattern = new RegExp("\\[color=(.*?)\\](.*?)\\[/color\\]", "g");
Expand All @@ -227,7 +366,7 @@
const boldTagPattern = new RegExp("\\[bold\\](.*?)\\[/bold\\]", "g");
const boldTagReplacement = "<strong>$1</strong>";
endTextElement.innerHTML = data.roundEndText.trim()
endTextElement.innerHTML = data.RoundEndText.trim()
.replaceAll(colorTagPattern, colorTagReplacement)
.replaceAll("\n", "<br>")
.replaceAll(boldTagPattern, boldTagReplacement);
Expand Down
66 changes: 57 additions & 9 deletions ReplayBrowser/Pages/Shared/ReplayDetails.razor
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,13 @@
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Players</h5>
@if(RequestModal != null)
{
<button class="btn-close" data-bs-target="#@RequestModal" data-bs-toggle="modal"></button>
} else
{
<button class="btn-close" data-bs-dismiss="modal"></button>
}
@if(RequestModal != null)
{
<button class="btn-close" data-bs-target="#@RequestModal" data-bs-toggle="modal"></button>
} else
{
<button class="btn-close" data-bs-dismiss="modal"></button>
}
</div>
<div class="modal-body">
@if (FullReplay is not null && FullReplay.RoundParticipants is not null)
Expand Down Expand Up @@ -112,12 +112,52 @@
</div>
</div>
</div>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#[email protected]" id="[email protected]">
View Timeline
</button>
<div class="modal fade modal-lg" id="[email protected]" tabindex="-1" aria-labelledby="[email protected]" aria-hidden="true" style="--bs-modal-width: 2000px;">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Events</h5>
@if(RequestModal != null)
{
<button class="btn-close" data-bs-target="#@RequestModal" data-bs-toggle="modal"></button>
} else
{
<button class="btn-close" data-bs-dismiss="modal"></button>
}
</div>
<div class="modal-body">
<div>
<div id="[email protected]"></div>
<div id="[email protected]"></div>
</div>
</div>
@if(RequestModal != null)
{
<div class="modal-footer">
<button class="btn btn-primary" data-bs-target="#@RequestModal" data-bs-toggle="modal">
Return
</button>
</div>
} else
{
<div class="modal-footer">
<button class="btn btn-primary" data-bs-dismiss="modal">
Close
</button>
</div>
}
</div>
</div>
</div>

<a href="@Replay.Link" target="_blank" class="btn btn-primary">Download</a>


@if (FullReplay is null || FullReplay.RoundParticipants is null || FullReplay.RoundEndText is null) {
<script>
@if (FullReplay is null || FullReplay.RoundParticipants is null || FullReplay.RoundEndText is null || FullReplay.Events is null) {
<script>
document.addEventListener('DOMContentLoaded', function() {
const modalPlayers = document.getElementById("[email protected]")
modalPlayers.addEventListener('click', function() {
Expand All @@ -136,6 +176,14 @@
if (endTextElement.innerHTML == "Loading...")
loadDetails(players, endTextElement, @Replay.Id);
});

const modalTimeline = document.getElementById("[email protected]")
modalTimeline.addEventListener('click', function() {
const timeline = document.getElementById("[email protected]");
const timelineDetails = document.getElementById("[email protected]");
if (timeline.innerHTML == "")
loadTimeline(timeline, @Replay.Id, timelineDetails);
});
});
</script>
}
Expand Down
Loading

0 comments on commit efb6871

Please sign in to comment.