Improvements to website comments and good grief reports along with numerous bug fixes

This commit is contained in:
Slendy 2022-02-12 02:06:48 -06:00
parent e4b4984505
commit aacc2d5eaf
30 changed files with 882 additions and 148 deletions

View file

@ -33,49 +33,9 @@ public class CommentController : ControllerBase
User? user = await this.database.UserFromGameRequest(this.Request); User? user = await this.database.UserFromGameRequest(this.Request);
if (user == null) return this.StatusCode(403, ""); if (user == null) return this.StatusCode(403, "");
Comment? comment = await this.database.Comments.Include(c => c.Poster).FirstOrDefaultAsync(c => commentId == c.CommentId); bool success = await this.database.RateComment(user, commentId, rating);
if (!success) return this.BadRequest();
if (comment == null) return this.BadRequest();
Reaction? reaction = await this.database.Reactions.FirstOrDefaultAsync(r => r.UserId == user.UserId && r.TargetId == commentId);
if (reaction == null)
{
Reaction newReaction = new Reaction()
{
UserId = user.UserId,
TargetId = commentId,
Rating = 0,
};
this.database.Reactions.Add(newReaction);
await this.database.SaveChangesAsync();
reaction = newReaction;
}
int oldRating = reaction.Rating;
if (oldRating == rating) return this.Ok();
reaction.Rating = rating;
// if rating changed then we count the number of reactions to ensure accuracy
List<Reaction> reactions = await this.database.Reactions
.Where(c => c.TargetId == commentId)
.ToListAsync();
int yay = 0;
int boo = 0;
foreach (Reaction r in reactions)
{
switch (r.Rating)
{
case -1:
boo++;
break;
case 1:
yay++;
break;
}
}
comment.ThumbsDown = boo;
comment.ThumbsUp = yay;
await this.database.SaveChangesAsync();
return this.Ok(); return this.Ok();
} }
@ -135,27 +95,12 @@ public class CommentController : ControllerBase
int targetId = slotId.GetValueOrDefault(); int targetId = slotId.GetValueOrDefault();
if (type == CommentType.Profile) if (type == CommentType.Profile) targetId = this.database.Users.First(u => u.Username == username).UserId;
{
User? target = await this.database.Users.FirstOrDefaultAsync(u => u.Username == username);
if (target == null) return this.BadRequest();
targetId = target.UserId;
}
else
{
Slot? target = await this.database.Slots.FirstOrDefaultAsync(u => u.SlotId == slotId);
if (target == null) return this.BadRequest();
}
comment.PosterUserId = poster.UserId; bool success = await this.database.PostComment(poster, targetId, type, comment.Message);
comment.TargetId = targetId; if (success) return this.Ok();
comment.Type = type;
comment.Timestamp = TimeHelper.UnixTimeMilliseconds(); return this.BadRequest();
this.database.Comments.Add(comment);
await this.database.SaveChangesAsync();
return this.Ok();
} }
[HttpPost("deleteUserComment/{username}")] [HttpPost("deleteUserComment/{username}")]

View file

@ -0,0 +1,56 @@
#nullable enable
using System;
using System.IO;
using System.Text.Json;
using System.Threading.Tasks;
using System.Xml.Serialization;
using Kettu;
using LBPUnion.ProjectLighthouse.Types;
using LBPUnion.ProjectLighthouse.Types.Reports;
using Microsoft.AspNetCore.Mvc;
using System.Text.Json;
using System.Text.Json.Serialization;
using LBPUnion.ProjectLighthouse.Helpers;
namespace LBPUnion.ProjectLighthouse.Controllers.GameApi;
[ApiController]
[Route("LITTLEBIGPLANETPS3_XML/")]
[Produces("text/xml")]
public class ReportController : ControllerBase
{
private readonly Database database;
public ReportController(Database database)
{
this.database = database;
}
[HttpPost("grief")]
public async Task<IActionResult> Report()
{
User? user = await this.database.UserFromGameRequest(this.Request);
if (user == null) return this.StatusCode(403, "");
this.Request.Body.Position = 0;
string bodyString = await new StreamReader(this.Request.Body).ReadToEndAsync();
Console.WriteLine(bodyString);
XmlSerializer serializer = new(typeof(GriefReport));
GriefReport? report = (GriefReport?) serializer.Deserialize(new StringReader(bodyString));
if (report == null) return this.BadRequest();
report.Bounds = JsonSerializer.Serialize(report.XmlBounds.Rect, typeof(Rectangle));
report.Players = JsonSerializer.Serialize(report.XmlPlayers, typeof(ReportPlayer[]));
report.VisiblePlayers = JsonSerializer.Serialize(report.XmlVisiblePlayers, typeof(VisiblePlayer[]));
report.Timestamp = TimeHelper.UnixTimeMilliseconds();
report.ReportingPlayerId = user.UserId;
this.database.Reports.Add(report);
await this.database.SaveChangesAsync();
return this.Ok();
}
}

View file

@ -55,7 +55,7 @@ public class ResourcesController : ControllerBase
[HttpGet("/gameAssets/{hash}")] [HttpGet("/gameAssets/{hash}")]
public IActionResult GetGameImage(string hash) public IActionResult GetGameImage(string hash)
{ {
string path = $"png/{hash}.png"; string path = $"png{Path.DirectorySeparatorChar}{hash}.png";
if (IOFile.Exists(path)) if (IOFile.Exists(path))
{ {

View file

@ -23,6 +23,32 @@ public class SlotPageController : ControllerBase
this.database = database; this.database = database;
} }
[HttpGet("rateComment")]
public async Task<IActionResult> RateComment([FromRoute] int id, [FromQuery] int commentId, [FromQuery] int rating)
{
User? user = this.database.UserFromWebRequest(this.Request);
if (user == null) return this.Redirect("~/login");
await this.database.RateComment(user,
commentId,
rating);
return this.Redirect("~/slot/" + id + "#" + commentId);
}
[HttpGet("postComment")]
public async Task<IActionResult> PostComment([FromRoute] int id, [FromQuery] string msg)
{
User? user = this.database.UserFromWebRequest(this.Request);
if (user == null) return this.Redirect("~/login");
bool success = await this.database.PostComment(user, id, CommentType.Level, msg);
if (!success) return this.NotFound();
return this.Redirect("~/slot/" + id);
}
[HttpGet("heart")] [HttpGet("heart")]
public async Task<IActionResult> HeartLevel([FromRoute] int id, [FromQuery] string? callbackUrl) public async Task<IActionResult> HeartLevel([FromRoute] int id, [FromQuery] string? callbackUrl)
{ {

View file

@ -1,7 +1,10 @@
#nullable enable #nullable enable
using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using LBPUnion.ProjectLighthouse.Types; using LBPUnion.ProjectLighthouse.Types;
using LBPUnion.ProjectLighthouse.Types.Profiles;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.CodeAnalysis;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Controllers.Website; namespace LBPUnion.ProjectLighthouse.Controllers.Website;
@ -31,6 +34,30 @@ public class UserPageController : ControllerBase
return this.Redirect("~/user/" + id); return this.Redirect("~/user/" + id);
} }
[HttpGet("rateComment")]
public async Task<IActionResult> RateComment([FromRoute] int id, [FromQuery] int commentId, [FromQuery] int rating)
{
User? user = this.database.UserFromWebRequest(this.Request);
if (user == null) return this.Redirect("~/login");
await this.database.RateComment(user, commentId, rating);
return this.Redirect("~/user/" + id + "#" + commentId);
}
[HttpGet("postComment")]
public async Task<IActionResult> PostComment([FromRoute] int id, [FromQuery] string msg)
{
User? user = this.database.UserFromWebRequest(this.Request);
if (user == null) return this.Redirect("~/login");
bool success = await this.database.PostComment(user, id, CommentType.Profile, msg);
if (!success) return this.NotFound();
return this.Redirect("~/user/" + id);
}
[HttpGet("unheart")] [HttpGet("unheart")]
public async Task<IActionResult> UnheartUser([FromRoute] int id) public async Task<IActionResult> UnheartUser([FromRoute] int id)
{ {

View file

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using LBPUnion.ProjectLighthouse.Helpers; using LBPUnion.ProjectLighthouse.Helpers;
@ -6,6 +7,7 @@ using LBPUnion.ProjectLighthouse.Types;
using LBPUnion.ProjectLighthouse.Types.Categories; using LBPUnion.ProjectLighthouse.Types.Categories;
using LBPUnion.ProjectLighthouse.Types.Levels; using LBPUnion.ProjectLighthouse.Types.Levels;
using LBPUnion.ProjectLighthouse.Types.Profiles; using LBPUnion.ProjectLighthouse.Types.Profiles;
using LBPUnion.ProjectLighthouse.Types.Reports;
using LBPUnion.ProjectLighthouse.Types.Reviews; using LBPUnion.ProjectLighthouse.Types.Reviews;
using LBPUnion.ProjectLighthouse.Types.Settings; using LBPUnion.ProjectLighthouse.Types.Settings;
using LBPUnion.ProjectLighthouse.Types.Tickets; using LBPUnion.ProjectLighthouse.Types.Tickets;
@ -37,6 +39,7 @@ public class Database : DbContext
public DbSet<UserApprovedIpAddress> UserApprovedIpAddresses { get; set; } public DbSet<UserApprovedIpAddress> UserApprovedIpAddresses { get; set; }
public DbSet<DatabaseCategory> CustomCategories { get; set; } public DbSet<DatabaseCategory> CustomCategories { get; set; }
public DbSet<Reaction> Reactions { get; set; } public DbSet<Reaction> Reactions { get; set; }
public DbSet<GriefReport> Reports { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder options) protected override void OnConfiguring(DbContextOptionsBuilder options)
=> options.UseMySql(ServerSettings.Instance.DbConnectionString, MySqlServerVersion.LatestSupportedServerVersion); => options.UseMySql(ServerSettings.Instance.DbConnectionString, MySqlServerVersion.LatestSupportedServerVersion);
@ -89,6 +92,83 @@ public class Database : DbContext
#region Hearts & Queues #region Hearts & Queues
public async Task<bool> RateComment(User user, int commentId, int rating)
{
Comment? comment = await this.Comments.FirstOrDefaultAsync(c => commentId == c.CommentId);
if (comment == null) return false;
if (comment.PosterUserId == user.UserId) return false;
Reaction? reaction = await this.Reactions.FirstOrDefaultAsync(r => r.UserId == user.UserId && r.TargetId == commentId);
if (reaction == null)
{
Reaction newReaction = new()
{
UserId = user.UserId,
TargetId = commentId,
Rating = 0,
};
this.Reactions.Add(newReaction);
await this.SaveChangesAsync();
reaction = newReaction;
}
int oldRating = reaction.Rating;
if (oldRating == rating) return true;
reaction.Rating = rating;
// if rating changed then we count the number of reactions to ensure accuracy
List<Reaction> reactions = await this.Reactions.Where(c => c.TargetId == commentId).ToListAsync();
int yay = 0;
int boo = 0;
foreach (Reaction r in reactions)
{
switch (r.Rating)
{
case -1:
boo++;
break;
case 1:
yay++;
break;
}
}
comment.ThumbsDown = boo;
comment.ThumbsUp = yay;
await this.SaveChangesAsync();
return true;
}
public async Task<bool> PostComment(User user, int targetId, CommentType type, string message)
{
if (type == CommentType.Profile)
{
User? targetUser = await this.Users.FirstOrDefaultAsync(u => u.UserId == targetId);
if (targetUser == null) return false;
}
else
{
Slot? targetSlot = await this.Slots.FirstOrDefaultAsync(u => u.SlotId == targetId);
if(targetSlot == null) return false;
}
this.Comments.Add
(
new Comment
{
PosterUserId = user.UserId,
TargetId = targetId,
Type = type,
Message = message,
Timestamp = TimeHelper.UnixTimeMilliseconds(),
}
);
await this.SaveChangesAsync();
return true;
}
public async Task HeartUser(User user, User heartedUser) public async Task HeartUser(User user, User heartedUser)
{ {
HeartedProfile? heartedProfile = await this.HeartedProfiles.FirstOrDefaultAsync(q => q.UserId == user.UserId && q.HeartedUserId == heartedUser.UserId); HeartedProfile? heartedProfile = await this.HeartedProfiles.FirstOrDefaultAsync(q => q.UserId == user.UserId && q.HeartedUserId == heartedUser.UserId);

View file

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Runtime.Intrinsics.Arm;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -11,7 +12,7 @@ namespace LBPUnion.ProjectLighthouse.Helpers;
[SuppressMessage("ReSharper", "UnusedMember.Global")] [SuppressMessage("ReSharper", "UnusedMember.Global")]
public static class HashHelper public static class HashHelper
{ {
private static readonly SHA1 sha1 = SHA1.Create(); // private static readonly SHA1 sha1 = SHA1.Create();
private static readonly SHA256 sha256 = SHA256.Create(); private static readonly SHA256 sha256 = SHA256.Create();
private static readonly Random random = new(); private static readonly Random random = new();
@ -71,7 +72,7 @@ public static class HashHelper
public static string Sha1Hash(string str) => Sha1Hash(Encoding.UTF8.GetBytes(str)); public static string Sha1Hash(string str) => Sha1Hash(Encoding.UTF8.GetBytes(str));
public static string Sha1Hash(byte[] bytes) => BitConverter.ToString(sha1.ComputeHash(bytes)).Replace("-", ""); public static string Sha1Hash(byte[] bytes) => BitConverter.ToString(SHA1.Create().ComputeHash(bytes)).Replace("-","");
public static string BCryptHash(string str) => BCrypt.Net.BCrypt.HashPassword(str); public static string BCryptHash(string str) => BCrypt.Net.BCrypt.HashPassword(str);

View file

@ -18,7 +18,7 @@ public static class ImageHelper
{ {
if (type != LbpFileType.Jpeg && type != LbpFileType.Png && type != LbpFileType.Texture) return false; if (type != LbpFileType.Jpeg && type != LbpFileType.Png && type != LbpFileType.Texture) return false;
if (File.Exists($"png/{hash}.png")) return true; if (File.Exists($"png{Path.DirectorySeparatorChar}{hash}.png")) return true;
using MemoryStream ms = new(data); using MemoryStream ms = new(data);
using BinaryReader reader = new(ms); using BinaryReader reader = new(ms);

View file

@ -0,0 +1,67 @@
using LBPUnion.ProjectLighthouse;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ProjectLighthouse.Migrations
{
[DbContext(typeof(Database))]
[Migration("20220212041106_AddGriefReports")]
public partial class AddGriefReports : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Reports",
columns: table => new
{
ReportId = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
Type = table.Column<int>(type: "int", nullable: false),
Timestamp = table.Column<long>(type: "bigint", nullable: false),
VisiblePlayers = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
ReportingPlayerId = table.Column<int>(type: "int", nullable: false),
Players = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
GriefStateHash = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
LevelOwner = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
InitialStateHash = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
JpegHash = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
LevelId = table.Column<int>(type: "int", nullable: false),
LevelType = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
Bounds = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4")
},
constraints: table =>
{
table.PrimaryKey("PK_Reports", x => x.ReportId);
table.ForeignKey(
name: "FK_Reports_Users_ReportingPlayerId",
column: x => x.ReportingPlayerId,
principalTable: "Users",
principalColumn: "UserId",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "IX_Reports_ReportingPlayerId",
table: "Reports",
column: "ReportingPlayerId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Reports");
}
}
}

View file

@ -15,7 +15,7 @@ namespace ProjectLighthouse.Migrations
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder modelBuilder
.HasAnnotation("ProductVersion", "6.0.0") .HasAnnotation("ProductVersion", "6.0.2")
.HasAnnotation("Relational:MaxIdentifierLength", 64); .HasAnnotation("Relational:MaxIdentifierLength", 64);
modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.AuthenticationAttempt", b => modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.AuthenticationAttempt", b =>
@ -503,6 +503,55 @@ namespace ProjectLighthouse.Migrations
b.ToTable("Reactions"); b.ToTable("Reactions");
}); });
modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Reports.GriefReport", b =>
{
b.Property<int>("ReportId")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Bounds")
.HasColumnType("longtext");
b.Property<string>("GriefStateHash")
.HasColumnType("longtext");
b.Property<string>("InitialStateHash")
.HasColumnType("longtext");
b.Property<string>("JpegHash")
.HasColumnType("longtext");
b.Property<int>("LevelId")
.HasColumnType("int");
b.Property<string>("LevelOwner")
.HasColumnType("longtext");
b.Property<string>("LevelType")
.HasColumnType("longtext");
b.Property<string>("Players")
.HasColumnType("longtext");
b.Property<int>("ReportingPlayerId")
.HasColumnType("int");
b.Property<long>("Timestamp")
.HasColumnType("bigint");
b.Property<int>("Type")
.HasColumnType("int");
b.Property<string>("VisiblePlayers")
.HasColumnType("longtext");
b.HasKey("ReportId");
b.HasIndex("ReportingPlayerId");
b.ToTable("Reports");
});
modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Reviews.RatedReview", b => modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Reviews.RatedReview", b =>
{ {
b.Property<int>("RatedReviewId") b.Property<int>("RatedReviewId")
@ -862,6 +911,17 @@ namespace ProjectLighthouse.Migrations
b.Navigation("Poster"); b.Navigation("Poster");
}); });
modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Reports.GriefReport", b =>
{
b.HasOne("LBPUnion.ProjectLighthouse.Types.User", "ReportingPlayer")
.WithMany()
.HasForeignKey("ReportingPlayerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ReportingPlayer");
});
modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Reviews.RatedReview", b => modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Reviews.RatedReview", b =>
{ {
b.HasOne("LBPUnion.ProjectLighthouse.Types.Reviews.Review", "Review") b.HasOne("LBPUnion.ProjectLighthouse.Types.Reviews.Review", "Review")

View file

@ -19,6 +19,7 @@
@if (Model.User.IsAdmin) @if (Model.User.IsAdmin)
{ {
Model.NavigationItems.Add(new PageNavigationItem("Reports", "/reports/0", "exclamation circle"));
Model.NavigationItemsRight.Add(new PageNavigationItem("Admin Panel", "/admin", "cogs")); Model.NavigationItemsRight.Add(new PageNavigationItem("Admin Panel", "/admin", "cogs"));
} }
Model.NavigationItemsRight.Add(new PageNavigationItem("Log out", "/logout", "user alternate slash")); // should always be last Model.NavigationItemsRight.Add(new PageNavigationItem("Log out", "/logout", "user alternate slash")); // should always be last

View file

@ -0,0 +1,74 @@
@using System.IO
@using System.Web
@using LBPUnion.ProjectLighthouse.Types.Profiles
<div class="ui yellow segment" id="comments">
<style>
.comment {
width: 100%;
display: inline-block;
text-align: left;
}
.voting {
text-align: center;
display: inline-block;
float: left;
margin-right: 0.5em;
}
</style>
<h1>Comments</h1>
@if (Model.Comments.Count == 0)
{
<p>There are no comments.</p>
}
@for (int i = 0; i < Model.Comments.Count; i++)
{
Comment comment = Model.Comments[i];
DateTimeOffset timestamp = DateTimeOffset.FromUnixTimeSeconds(comment.Timestamp / 1000);
StringWriter messageWriter = new();
HttpUtility.HtmlDecode(comment.getComment(), messageWriter);
string decodedMessage = messageWriter.ToString();
string url = Url.RouteUrl(ViewContext.RouteData.Values);
int rating = comment.ThumbsUp - comment.ThumbsDown;
<div style="display: flex" id="@comment.CommentId">
<div class="voting">
<a href="@url/rateComment?commentId=@(comment.CommentId)&rating=@(comment.YourThumb == 1 ? 0 : 1)">
<i class="fitted @(comment.YourThumb == 1 ? "green" : "grey") arrow up link icon" style="display: block"></i>
</a>
<span style="text-align: center; margin: auto; @(rating < 0 ? "margin-left: -5px" : "")">@(rating)</span>
<a href="@url/rateComment?commentId=@(comment.CommentId)&rating=@(comment.YourThumb == -1 ? 0 : -1)">
<i class="fitted @(comment.YourThumb == -1 ? "red" : "grey") arrow down link icon" style="display: block"></i>
</a>
</div>
<div class="comment">
<b><a href="/user/@comment.PosterUserId">@comment.Poster.Username</a>: </b>
@if (comment.Deleted)
{
<i><span>@decodedMessage</span></i>
}
else
{
<span>@decodedMessage</span>
}
<p>
<i>@timestamp.ToString("MM/dd/yyyy @ h:mm tt") UTC</i>
</p>
@if (i != Model.Comments.Count - 1)
{
<div class="ui divider"></div>
}
</div>
</div>
}
@if(Model.CommentsEnabled){
<div class="ui divider"></div>
<form class="ui reply form" action="@Url.RouteUrl(ViewContext.RouteData.Values)/postComment">
<div class="field">
<textarea style="min-height: 70px; height: 70px; max-height:120px" name="msg"></textarea>
</div>
<input type="submit" class="ui blue button">
</form>
}
</div>

View file

@ -31,16 +31,17 @@ public class PhotosPage : BaseLayout
{ {
if (string.IsNullOrWhiteSpace(name)) name = ""; if (string.IsNullOrWhiteSpace(name)) name = "";
this.PhotoCount = await this.Database.Photos.CountAsync(p => p.Creator.Username.Contains(name) || p.PhotoSubjectCollection.Contains(name)); this.SearchValue = name.Replace(" ", string.Empty);
this.PhotoCount = await this.Database.Photos.CountAsync(p => p.Creator.Username.Contains(this.SearchValue) || p.PhotoSubjectCollection.Contains(this.SearchValue));
this.SearchValue = name;
this.PageNumber = pageNumber; this.PageNumber = pageNumber;
this.PageAmount = Math.Max(1, (int)Math.Ceiling((double)this.PhotoCount / ServerStatics.PageSize)); this.PageAmount = Math.Max(1, (int)Math.Ceiling((double)this.PhotoCount / ServerStatics.PageSize));
if (this.PageNumber < 0 || this.PageNumber >= this.PageAmount) return this.Redirect($"/photos/{Math.Clamp(this.PageNumber, 0, this.PageAmount - 1)}"); if (this.PageNumber < 0 || this.PageNumber >= this.PageAmount) return this.Redirect($"/photos/{Math.Clamp(this.PageNumber, 0, this.PageAmount - 1)}");
this.Photos = await this.Database.Photos.Include(p => p.Creator) this.Photos = await this.Database.Photos.Include(p => p.Creator)
.Where(p => p.Creator.Username.Contains(name) || p.PhotoSubjectCollection.Contains(name)) .Where(p => p.Creator.Username.Contains(this.SearchValue) || p.PhotoSubjectCollection.Contains(this.SearchValue))
.OrderByDescending(p => p.Timestamp) .OrderByDescending(p => p.Timestamp)
.Skip(pageNumber * ServerStatics.PageSize) .Skip(pageNumber * ServerStatics.PageSize)
.Take(ServerStatics.PageSize) .Take(ServerStatics.PageSize)

View file

@ -0,0 +1,206 @@
@page "/reports/{pageNumber:int}"
@using LBPUnion.ProjectLighthouse.Types.Reports
@model LBPUnion.ProjectLighthouse.Pages.ReportsPage
@{
Layout = "Layouts/BaseLayout";
Model.Title = "Reports";
}
<p>There are @Model.ReportCount total reports!</p>
<form action="/reports/0">
<div class="ui icon input">
<input type="text" name="name" placeholder="Search reports..." value="@Model.SearchValue">
<i class="search icon"></i>
</div>
</form>
<div class="ui divider"></div>
<script>
let subjects = [];
let bounds = [];
let canvases = [];
let ctx = [];
let images = [];
</script>
@foreach (GriefReport report in Model.Reports)
{
<div class="ui segment">
<div>
<canvas class="hide-subjects" id="canvas-subjects-@report.ReportId" width="1920" height="1080"
style="position: absolute; transform: rotate(180deg)">
</canvas>
<img id="game-image-@report.ReportId" src="/gameAssets/@report.JpegHash" alt="Grief report picture" style="width: 100%; height: auto; border-radius: .28571429rem;">
</div>
<p><i>Report submitted by <b><a href="/user/@report.ReportingPlayerId">@report.ReportingPlayer.Username</a></b></i></p>
<b class="hover-players" id="hover-subjects-2-@report.ReportId">Report contains @report.XmlPlayers.Length @(report.XmlPlayers.Length == 1 ? "player" : "players")</b>
@foreach (ReportPlayer player in report.XmlPlayers)
{
<div id="hover-subjects-@report.ReportId" class="hover-players">
<a href="/">@player.Name</a>
</div>
}
<div><b>Report time: </b>@(DateTimeOffset.FromUnixTimeMilliseconds(report.Timestamp).ToString("R"))</div>
<div><b>Report reason: </b>@report.Type</div>
<div><b>Level ID:</b> @report.LevelId</div>
<div><b>Level type:</b> @report.LevelType</div>
<div><b>Level owner:</b> @report.LevelOwner</div>
<div id="hover-bounds-@report.ReportId" class="hover-region"><b>Hover to see reported region</b></div>
</div>
<script>
subjects[@report.ReportId] = @Html.Raw(report.Players)
bounds[@report.ReportId] = @Html.Raw(report.Bounds)
images[@report.ReportId] = document.getElementById("game-image-@report.ReportId")
canvases[@report.ReportId] = document.getElementById("canvas-subjects-@report.ReportId")
canvases[@report.ReportId].width = images[@report.ReportId].offsetWidth;
canvases[@report.ReportId].height = images[@report.ReportId].clientHeight;
ctx[@report.ReportId] = canvases[@report.ReportId].getContext('2d');
</script>
}
<script>
function getReportId(name){
let split = name.split("-");
return split[split.length-1];
}
const colours = ["#96dd3c", "#ceb424", "#cc0a1d", "#c800cc"];
let displayType;
window.addEventListener("load", function () {
document.querySelectorAll(".hover-players").forEach(item => {
item.addEventListener('mouseenter', function () {
let reportId = getReportId(item.id);
displayType = 1;
canvases[reportId].className = "photo-subjects";
redraw(reportId);
});
});
document.querySelectorAll(".hover-region").forEach(item => {
item.addEventListener('mouseenter', function () {
let reportId = getReportId(item.id);
displayType = 0;
canvases[reportId].className = "photo-subjects";
redraw(reportId);
});
});
document.querySelectorAll(".hover-region, .hover-players").forEach(item => {
item.addEventListener('mouseleave', function () {
canvases[getReportId(item.id)].className = "photo-subjects hide-subjects";
});
});
}, false);
function redraw(reportId){
let context = ctx[reportId];
let canvas = canvases[reportId];
let image = images[reportId];
context.clearRect(0, 0, canvas.width, canvas.height);
let w = canvas.width;
let h = canvas.height;
// halfwidth, halfheight
const hw = w / 2;
const hh = h / 2;
switch (displayType){
case 0: {
let imageBounds = bounds[reportId];
const x1 = imageBounds.Left;
const x2 = imageBounds.Right;
const y1 = imageBounds.Top;
const y2 = imageBounds.Bottom;
const scaleX = image.naturalWidth / canvas.width;
const scaleY = image.naturalHeight / canvas.height;
const bx = canvas.width-(x2/scaleX);
const by = canvas.height-(y2/scaleY);
const bw = (x2 - x1) / scaleX;
const bh = (y2 - y1) / scaleY;
context.beginPath();
context.lineWidth = 3;
context.globalAlpha = 0.6;
context.fillStyle = "black";
context.fillRect(0, 0, canvas.width, canvas.height);
context.clearRect(bx, by, bw, bh);
context.globalAlpha = 1.0;
context.beginPath();
context.lineWidth = 3;
context.strokeStyle = "#cc0a1d";
context.rect(bx, by, bw, bh);
context.stroke();
break;
}
case 1: {
let subject = subjects[reportId];
subject.forEach((s, si) => {
const colour = colours[si % 4];
// Bounding box
const x1 = s.Location.Left;
const x2 = s.Location.Right;
const y1 = s.Location.Top;
const y2 = s.Location.Bottom;
const scaleX = image.naturalWidth / canvas.width;
const scaleY = image.naturalHeight / canvas.height;
const bx = canvas.width-(x2/scaleX);
const by = canvas.height-(y2/scaleY);
const bw = (x2 - x1) / scaleX;
const bh = (y2 - y1) / scaleY;
context.beginPath();
context.lineWidth = 3;
context.strokeStyle = colour;
context.rect(bx, by, bw, bh);
context.stroke();
// Move into relative coordinates from bounding box
context.translate(bx, by);
// Username label
context.font = "16px Lato";
context.fillStyle = colour;
// Text width/height for the label background
const tw = context.measureText(s.Name).width;
const th = 24;
// Check if the label will flow off the bottom of the frame
const overflowBottom = (y2+tw - 24) > (canvas.width);
// Check if the label will flow off the left of the frame
const overflowLeft = (x2) < (24);
// Set alignment
context.textAlign = overflowLeft ? "start" : "end";
// Text x / y
const lx = overflowLeft ? -bw + 6 : -6;
const ly = overflowBottom ? -bh - 6 : 16;
// Label background x / y
const lbx = overflowLeft ? bw - tw - 12 : 0;
const lby = overflowBottom ? bh : -24;
// Draw background
context.fillRect(lbx, lby, tw+8, th);
// Draw text, rotated back upright (canvas draws rotated 180deg)
context.fillStyle = "white";
context.rotate(Math.PI);
context.fillText(s.Name, lx, ly);
// reset transform
context.setTransform(1, 0, 0, 1, 0, 0);
});
break;
}
}
}
</script>
@if (Model.PageNumber != 0)
{
<a href="/reports/@(Model.PageNumber - 1)@(Model.SearchValue.Length == 0 ? "" : "?name=" + Model.SearchValue)">Previous Page</a>
}
@(Model.PageNumber + 1) / @(Model.PageAmount)
@if (Model.PageNumber < Model.PageAmount - 1)
{
<a href="/reports/@(Model.PageNumber + 1)@(Model.SearchValue.Length == 0 ? "" : "?name=" + Model.SearchValue)">Next Page</a>
}

View file

@ -0,0 +1,65 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using JetBrains.Annotations;
using LBPUnion.ProjectLighthouse.Pages.Layouts;
using LBPUnion.ProjectLighthouse.Types.Reports;
using LBPUnion.ProjectLighthouse.Types.Settings;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Pages;
public class ReportsPage : BaseLayout
{
public int PageAmount;
public int PageNumber;
public int ReportCount;
public List<GriefReport> Reports;
public string SearchValue;
public ReportsPage([NotNull] Database database) : base(database)
{ }
public async Task<IActionResult> OnGet([FromRoute] int pageNumber, [FromQuery] string? name)
{
if (string.IsNullOrWhiteSpace(name)) name = "";
this.SearchValue = name.Replace(" ", string.Empty);
this.ReportCount = await this.Database.Reports.Include(r => r.ReportingPlayer).CountAsync(r => r.ReportingPlayer.Username.Contains(this.SearchValue));
this.PageNumber = pageNumber;
this.PageAmount = Math.Max(1, (int) Math.Ceiling((double) this.ReportCount / ServerStatics.PageSize));
if (this.PageNumber < 0 || this.PageNumber >= this.PageAmount)
return this.Redirect($"/reports/{Math.Clamp(this.PageNumber, 0, this.PageAmount - 1)}");
this.Reports = await this.Database.Reports.Include(r => r.ReportingPlayer)
.Where(r => r.ReportingPlayer.Username.Contains(this.SearchValue))
.OrderByDescending(r => r.Timestamp)
.Skip(pageNumber * ServerStatics.PageSize)
.Take(ServerStatics.PageSize)
.ToListAsync();
foreach (GriefReport r in this.Reports)
{
r.XmlPlayers = (ReportPlayer[]) JsonSerializer.Deserialize(r.Players, typeof(ReportPlayer[]))!;
r.XmlBounds = new Marqee()
{
Rect = (Rectangle) JsonSerializer.Deserialize(r.Bounds, typeof(Rectangle))!,
};
}
return this.Page();
}
}

View file

@ -1,8 +1,5 @@
@page "/slot/{id:int}" @page "/slot/{id:int}"
@using System.IO
@using System.Web
@using LBPUnion.ProjectLighthouse.Helpers.Extensions @using LBPUnion.ProjectLighthouse.Helpers.Extensions
@using LBPUnion.ProjectLighthouse.Types.Profiles
@model LBPUnion.ProjectLighthouse.Pages.SlotPage @model LBPUnion.ProjectLighthouse.Pages.SlotPage
@{ @{
@ -57,36 +54,9 @@
</div> </div>
</div> </div>
</div> </div>
<div class="ui yellow segment">
<h1>Comments</h1>
@if (Model.Comments.Count == 0)
{
<p>There are no comments.</p>
}
@foreach (Comment comment in Model.Comments!) @await Html.PartialAsync("Partials/CommentsPartial")
{
DateTimeOffset timestamp = DateTimeOffset.FromUnixTimeSeconds(comment.Timestamp / 1000);
StringWriter messageWriter = new();
HttpUtility.HtmlDecode(comment.getComment(), messageWriter);
string decodedMessage = messageWriter.ToString();
<div>
<b><a href="/user/@comment.PosterUserId">@comment.Poster.Username</a>: </b>
@if (comment.Deleted)
{
<i><span>@decodedMessage</span></i>
}
else
{
<span>@decodedMessage</span>
}
<p>
<i>@timestamp.ToString("MM/dd/yyyy @ h:mm tt") UTC</i>
</p>
<div class="ui divider"></div>
</div>
}
</div>
@if (Model.User != null && Model.User.IsAdmin) @if (Model.User != null && Model.User.IsAdmin)
{ {
<div class="ui yellow segment"> <div class="ui yellow segment">

View file

@ -7,6 +7,7 @@ using LBPUnion.ProjectLighthouse.Pages.Layouts;
using LBPUnion.ProjectLighthouse.Types; using LBPUnion.ProjectLighthouse.Types;
using LBPUnion.ProjectLighthouse.Types.Levels; using LBPUnion.ProjectLighthouse.Types.Levels;
using LBPUnion.ProjectLighthouse.Types.Profiles; using LBPUnion.ProjectLighthouse.Types.Profiles;
using LBPUnion.ProjectLighthouse.Types.Settings;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@ -16,6 +17,8 @@ public class SlotPage : BaseLayout
{ {
public List<Comment> Comments; public List<Comment> Comments;
public bool CommentsEnabled = ServerSettings.Instance.LevelCommentsEnabled;
public Slot Slot; public Slot Slot;
public SlotPage([NotNull] Database database) : base(database) public SlotPage([NotNull] Database database) : base(database)
{} {}
@ -33,6 +36,15 @@ public class SlotPage : BaseLayout
this.Slot = slot; this.Slot = slot;
if (this.User == null) return this.Page();
foreach (Comment c in this.Comments)
{
Reaction? reaction = await this.Database.Reactions.FirstOrDefaultAsync(r =>
r.UserId == this.User.UserId && r.TargetId == c.CommentId);
if (reaction != null) c.YourThumb = reaction.Rating;
}
return this.Page(); return this.Page();
} }
} }

View file

@ -32,16 +32,17 @@ public class SlotsPage : BaseLayout
{ {
if (string.IsNullOrWhiteSpace(name)) name = ""; if (string.IsNullOrWhiteSpace(name)) name = "";
this.SlotCount = await this.Database.Slots.CountAsync(p => p.Name.Contains(name)); this.SearchValue = name.Replace(" ", string.Empty);
this.SlotCount = await this.Database.Slots.CountAsync(p => p.Name.Contains(this.SearchValue));
this.SearchValue = name;
this.PageNumber = pageNumber; this.PageNumber = pageNumber;
this.PageAmount = Math.Max(1, (int)Math.Ceiling((double)this.SlotCount / ServerStatics.PageSize)); this.PageAmount = Math.Max(1, (int)Math.Ceiling((double)this.SlotCount / ServerStatics.PageSize));
if (this.PageNumber < 0 || this.PageNumber >= this.PageAmount) return this.Redirect($"/slots/{Math.Clamp(this.PageNumber, 0, this.PageAmount - 1)}"); if (this.PageNumber < 0 || this.PageNumber >= this.PageAmount) return this.Redirect($"/slots/{Math.Clamp(this.PageNumber, 0, this.PageAmount - 1)}");
this.Slots = await this.Database.Slots.Include(p => p.Creator) this.Slots = await this.Database.Slots.Include(p => p.Creator)
.Where(p => p.Name.Contains(name)) .Where(p => p.Name.Contains(this.SearchValue))
.OrderByDescending(p => p.FirstUploaded) .OrderByDescending(p => p.FirstUploaded)
.Skip(pageNumber * ServerStatics.PageSize) .Skip(pageNumber * ServerStatics.PageSize)
.Take(ServerStatics.PageSize) .Take(ServerStatics.PageSize)

View file

@ -1,9 +1,6 @@
@page "/user/{userId:int}" @page "/user/{userId:int}"
@using System.IO
@using System.Web
@using LBPUnion.ProjectLighthouse.Helpers.Extensions @using LBPUnion.ProjectLighthouse.Helpers.Extensions
@using LBPUnion.ProjectLighthouse.Types @using LBPUnion.ProjectLighthouse.Types
@using LBPUnion.ProjectLighthouse.Types.Profiles
@model LBPUnion.ProjectLighthouse.Pages.UserPage @model LBPUnion.ProjectLighthouse.Pages.UserPage
@{ @{
@ -114,35 +111,4 @@
</div> </div>
} }
@await Html.PartialAsync("Partials/CommentsPartial")
<div class="ui yellow segment">
<h1>Comments</h1>
@if (Model.ProfileUser.Comments == 0)
{
<p>There are no comments.</p>
}
@foreach (Comment comment in Model.Comments!)
{
DateTimeOffset timestamp = DateTimeOffset.FromUnixTimeSeconds(comment.Timestamp / 1000);
StringWriter messageWriter = new();
HttpUtility.HtmlDecode(comment.getComment(), messageWriter);
string decodedMessage = messageWriter.ToString();
<div>
<b><a href="/user/@comment.PosterUserId">@comment.Poster.Username</a>: </b>
@if (comment.Deleted)
{
<i><span>@decodedMessage</span></i>
}
else
{
<span>@decodedMessage</span>
}
<p>
<i>@timestamp.ToString("MM/dd/yyyy @ h:mm tt") UTC</i>
</p>
<div class="ui divider"></div>
</div>
}
</div>

View file

@ -5,6 +5,7 @@ using System.Threading.Tasks;
using LBPUnion.ProjectLighthouse.Pages.Layouts; using LBPUnion.ProjectLighthouse.Pages.Layouts;
using LBPUnion.ProjectLighthouse.Types; using LBPUnion.ProjectLighthouse.Types;
using LBPUnion.ProjectLighthouse.Types.Profiles; using LBPUnion.ProjectLighthouse.Types.Profiles;
using LBPUnion.ProjectLighthouse.Types.Settings;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@ -14,6 +15,8 @@ public class UserPage : BaseLayout
{ {
public List<Comment>? Comments; public List<Comment>? Comments;
public bool CommentsEnabled = ServerSettings.Instance.ProfileCommentsEnabled;
public bool IsProfileUserHearted; public bool IsProfileUserHearted;
public List<Photo>? Photos; public List<Photo>? Photos;
@ -34,11 +37,19 @@ public class UserPage : BaseLayout
.Where(p => p.TargetId == userId && p.Type == CommentType.Profile) .Where(p => p.TargetId == userId && p.Type == CommentType.Profile)
.Take(50) .Take(50)
.ToListAsync(); .ToListAsync();
if (this.User != null) if (this.User != null)
this.IsProfileUserHearted = await this.Database.HeartedProfiles.FirstOrDefaultAsync {
(u => u.UserId == this.User.UserId && u.HeartedUserId == this.ProfileUser.UserId) != foreach (Comment c in this.Comments)
{
Reaction? reaction = await this.Database.Reactions.FirstOrDefaultAsync(r =>
r.UserId == this.User.UserId && r.TargetId == c.CommentId);
if (reaction != null) c.YourThumb = reaction.Rating;
}
this.IsProfileUserHearted = await this.Database.HeartedProfiles.FirstOrDefaultAsync(u =>
u.UserId == this.User.UserId &&
u.HeartedUserId == this.ProfileUser.UserId) !=
null; null;
}
return this.Page(); return this.Page();
} }

View file

@ -31,15 +31,16 @@ public class UsersPage : BaseLayout
{ {
if (string.IsNullOrWhiteSpace(name)) name = ""; if (string.IsNullOrWhiteSpace(name)) name = "";
this.UserCount = await this.Database.Users.CountAsync(u => !u.Banned && u.Username.Contains(name)); this.SearchValue = name.Replace(" ", string.Empty);
this.UserCount = await this.Database.Users.CountAsync(u => !u.Banned && u.Username.Contains(this.SearchValue));
this.SearchValue = name;
this.PageNumber = pageNumber; this.PageNumber = pageNumber;
this.PageAmount = Math.Max(1, (int)Math.Ceiling((double)this.UserCount / ServerStatics.PageSize)); this.PageAmount = Math.Max(1, (int)Math.Ceiling((double)this.UserCount / ServerStatics.PageSize));
if (this.PageNumber < 0 || this.PageNumber >= this.PageAmount) return this.Redirect($"/users/{Math.Clamp(this.PageNumber, 0, this.PageAmount - 1)}"); if (this.PageNumber < 0 || this.PageNumber >= this.PageAmount) return this.Redirect($"/users/{Math.Clamp(this.PageNumber, 0, this.PageAmount - 1)}");
this.Users = await this.Database.Users.Where(u => !u.Banned && u.Username.Contains(name)) this.Users = await this.Database.Users.Where(u => !u.Banned && u.Username.Contains(this.SearchValue))
.OrderByDescending(b => b.UserId) .OrderByDescending(b => b.UserId)
.Skip(pageNumber * ServerStatics.PageSize) .Skip(pageNumber * ServerStatics.PageSize)
.Take(ServerStatics.PageSize) .Take(ServerStatics.PageSize)

View file

@ -100,7 +100,7 @@ public static class Program
{ {
while (fileQueue.TryDequeue(out string? filename)) while (fileQueue.TryDequeue(out string? filename))
{ {
LbpFile? file = LbpFile.FromHash(filename.Replace("r/", "")); LbpFile? file = LbpFile.FromHash(filename.Replace("r" + Path.DirectorySeparatorChar, ""));
if (file == null) continue; if (file == null) continue;
if (file.FileType == LbpFileType.Jpeg || file.FileType == LbpFileType.Png || file.FileType == LbpFileType.Texture) if (file.FileType == LbpFileType.Jpeg || file.FileType == LbpFileType.Png || file.FileType == LbpFileType.Texture)

View file

@ -37,6 +37,10 @@ public class Comment
public int ThumbsUp { get; set; } public int ThumbsUp { get; set; }
public int ThumbsDown { get; set; } public int ThumbsDown { get; set; }
[NotMapped]
[XmlIgnore]
public int YourThumb;
public string getComment() public string getComment()
{ {
if (!this.Deleted) if (!this.Deleted)

View file

@ -0,0 +1,59 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Xml.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.Reports;
[XmlRoot("griefReport")]
public class GriefReport
{
[Key]
public int ReportId { get; set; }
[XmlElement("griefTypeId")]
public GriefType Type { get; set; }
public long Timestamp { get; set; }
[NotMapped]
[XmlElement("visibleBadge")]
public VisiblePlayer[] XmlVisiblePlayers { get; set; }
public string VisiblePlayers { get; set; }
public int ReportingPlayerId { get; set; }
[ForeignKey(nameof(ReportingPlayerId))]
public User ReportingPlayer { get; set; }
[NotMapped]
[XmlElement("player")]
public ReportPlayer[] XmlPlayers { get; set; }
public string Players { get; set; }
[XmlElement("griefStateHash")]
public string GriefStateHash { get; set; }
[XmlElement("levelOwner")]
public string LevelOwner { get; set; }
[XmlElement("initialStateHash")]
public string InitialStateHash { get; set; }
[XmlElement("jpegHash")]
public string JpegHash { get; set; }
[XmlElement("levelId")]
public int LevelId { get; set; }
[XmlElement("levelType")]
public string LevelType { get; set; }
[NotMapped]
[XmlElement("marqee")]
public Marqee XmlBounds { get; set; }
public string Bounds { get; set; }
}

View file

@ -0,0 +1,23 @@
using System.Xml.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.Reports;
public enum GriefType
{
[XmlEnum("1")]
Obscene = 1,
[XmlEnum("2")]
Mature = 2,
[XmlEnum("3")]
Offensive = 3,
[XmlEnum("4")]
Violence = 4,
[XmlEnum("5")]
Illegal = 5,
[XmlEnum("6")]
Unknown = 6,
[XmlEnum("7")]
Tos = 7,
[XmlEnum("8")]
Other = 8,
}

View file

@ -0,0 +1,10 @@
using System.Xml.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.Reports;
[XmlRoot("marqee")]
public class Marqee
{
[XmlElement("rect")]
public Rectangle Rect { get; set; }
}

View file

@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Xml.Serialization;
using LBPUnion.ProjectLighthouse.Serialization;
using Newtonsoft.Json;
namespace LBPUnion.ProjectLighthouse.Types.Reports;
public class Rectangle
{
[XmlAttribute("t")]
public int Top { get; set; }
[XmlAttribute("l")]
public int Left { get; set; }
[XmlAttribute("b")]
public int Bottom { get; set; }
[XmlAttribute("r")]
public int Right { get; set; }
}

View file

@ -0,0 +1,23 @@
using System.Xml.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.Reports;
[XmlRoot("player")]
public class ReportPlayer
{
[XmlElement("id")]
public string Name { get; set; }
[XmlElement("rect")]
public Rectangle Location { get; set; }
[XmlAttribute("reporter")]
public bool Reporter { get; set; }
[XmlAttribute("ingamenow")]
public bool InGame { get; set; }
[XmlAttribute("playerNumber")]
public int PlayerNum { get; set; }
}

View file

@ -0,0 +1,22 @@
using System;
using System.Xml.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.Reports;
[XmlRoot("visibleBadge")]
[Serializable]
public class VisiblePlayer
{
[XmlElement("id")]
public string Name { get; set; }
[XmlElement("hash")]
public string Hash { get; set; }
[XmlElement("rect")]
public Rectangle Bounds { get; set; }
[XmlAttribute("type")]
public string Type { get; set; }
}

View file

@ -190,8 +190,8 @@ public class User
LbpSerializer.BlankElement("photos") + LbpSerializer.BlankElement("photos") +
LbpSerializer.StringElement("heartCount", this.Hearts) + LbpSerializer.StringElement("heartCount", this.Hearts) +
LbpSerializer.StringElement("yay2", this.YayHash) + LbpSerializer.StringElement("yay2", this.YayHash) +
LbpSerializer.StringElement("boo2", this.YayHash) + LbpSerializer.StringElement("boo2", this.BooHash) +
LbpSerializer.StringElement("meh2", this.YayHash); LbpSerializer.StringElement("meh2", this.MehHash);
this.ClientsConnected.Serialize(); this.ClientsConnected.Serialize();
return LbpSerializer.TaggedStringElement("user", user, "type", "user"); return LbpSerializer.TaggedStringElement("user", user, "type", "user");