LittleBigPlanet 3 Adventure Support (#477)

* Baseline LBP3 Adventure slot support
VERY unsafe and hacky to use as of now, this is just testing the waters.

* ADC file type checking

* Refactor & trimming
This might need to be adjusted if any feature is found to be missing

* isAdventure added to API

* Prototype Adventure icons for Website
I am not an artist, please make this more in line with the originals.

* Override border radius for LBP3 Adventures

* Cleaning

* Remove WriteLine and unused property

* Remove unused libraries

* Handle tracking and submitting of Adventure scores

* Check for null instead of 0
Non-adventure slots will report null, not 0

* Score for adventure slot instead of main slot

* Tweaks for PR

* Identify levels for photos by level resource
Verify this doesn't break anything.

* SlotCardPartial merge with main changes

* PR resolution 2

* probably not what was wanted
Use variables for style extension

* Internal slots already properly identified

* Return line breaks to end of Slot.cs

* Remove line break added by Github

thanks

* Github.

* Make this a one-liner

* Reduce to two lines

* This can also be one line

* This can *also* be one line

* Update ProjectLighthouse.Servers.Website/Pages/Partials/SlotCardPartial.cshtml

Co-authored-by: Josh <josh@slendy.pw>

* PR changes

* Update ProjectLighthouse/Migrations/20220916141401_ScoreboardAdvSlot.cs

Co-authored-by: Josh <josh@slendy.pw>

Co-authored-by: Josh <josh@slendy.pw>
This commit is contained in:
Dagg 2022-09-20 14:08:02 -07:00 committed by GitHub
parent 83a905c8a2
commit dfd1d9b748
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 107 additions and 22 deletions

View file

@ -10,6 +10,7 @@ public struct MinimalSlot
public string Name { get; set; } public string Name { get; set; }
public string IconHash { get; set; } public string IconHash { get; set; }
public bool TeamPick { get; set; } public bool TeamPick { get; set; }
public bool IsAdventure { get; set; }
public GameVersion GameVersion { get; set; } public GameVersion GameVersion { get; set; }
#if DEBUG #if DEBUG
public long FirstUploaded { get; set; } public long FirstUploaded { get; set; }
@ -22,6 +23,7 @@ public struct MinimalSlot
Name = slot.Name, Name = slot.Name,
IconHash = slot.IconHash, IconHash = slot.IconHash,
TeamPick = slot.TeamPick, TeamPick = slot.TeamPick,
IsAdventure = slot.IsAdventurePlanet,
GameVersion = slot.GameVersion, GameVersion = slot.GameVersion,
#if DEBUG #if DEBUG
FirstUploaded = slot.FirstUploaded, FirstUploaded = slot.FirstUploaded,

View file

@ -63,8 +63,12 @@ public class PhotosController : ControllerBase
{ {
case SlotType.User: case SlotType.User:
{ {
Slot? slot = await this.database.Slots.FirstOrDefaultAsync(s => s.Type == SlotType.User && s.SlotId == photoSlot.SlotId); // We'll grab the slot by the RootLevel and see what happens from here.
if (slot != null && !string.IsNullOrEmpty(slot.RootLevel)) validLevel = true; Slot? slot = await this.database.Slots.FirstOrDefaultAsync(s => s.Type == SlotType.User && s.ResourceCollection.Contains(photoSlot.RootLevel));
if(slot == null) break;
if (!string.IsNullOrEmpty(slot!.RootLevel)) validLevel = true;
if (slot.IsAdventurePlanet) photoSlot.SlotId = slot.SlotId;
break; break;
} }
case SlotType.Pod: case SlotType.Pod:

View file

@ -40,7 +40,8 @@ public class PublishController : ControllerBase
GameToken gameToken = userAndToken.Value.Item2; GameToken gameToken = userAndToken.Value.Item2;
Slot? slot = await this.getSlotFromBody(); Slot? slot = await this.getSlotFromBody();
if (slot == null) { if (slot == null)
{
Logger.Warn("Rejecting level upload, slot is null", LogArea.Publish); Logger.Warn("Rejecting level upload, slot is null", LogArea.Publish);
return this.BadRequest(); // if the level cant be parsed then it obviously cant be uploaded return this.BadRequest(); // if the level cant be parsed then it obviously cant be uploaded
} }
@ -135,11 +136,22 @@ public class PublishController : ControllerBase
return this.BadRequest(); return this.BadRequest();
} }
if (!slot.IsAdventurePlanet)
{
if (rootLevel.FileType != LbpFileType.Level) if (rootLevel.FileType != LbpFileType.Level)
{ {
Logger.Warn("Rejecting level upload, rootLevel is not a level", LogArea.Publish); Logger.Warn("Rejecting level upload, rootLevel is not a level", LogArea.Publish);
return this.BadRequest(); return this.BadRequest();
} }
}
else
{
if (rootLevel.FileType != LbpFileType.Adventure)
{
Logger.Warn("Rejecting level upload, rootLevel is not a LBP 3 Adventure", LogArea.Publish);
return this.BadRequest();
}
}
GameVersion slotVersion = FileHelper.ParseLevelVersion(rootLevel); GameVersion slotVersion = FileHelper.ParseLevelVersion(rootLevel);

View file

@ -26,7 +26,8 @@ public class ScoreController : ControllerBase
} }
[HttpPost("scoreboard/{slotType}/{id:int}")] [HttpPost("scoreboard/{slotType}/{id:int}")]
public async Task<IActionResult> SubmitScore(string slotType, int id, [FromQuery] bool lbp1 = false, [FromQuery] bool lbp2 = false, [FromQuery] bool lbp3 = false) [HttpPost("scoreboard/{slotType}/{id:int}/{childId:int}")]
public async Task<IActionResult> SubmitScore(string slotType, int id, int? childId, [FromQuery] bool lbp1 = false, [FromQuery] bool lbp2 = false, [FromQuery] bool lbp3 = false)
{ {
GameToken? token = await this.database.GameTokenFromRequest(this.Request); GameToken? token = await this.database.GameTokenFromRequest(this.Request);
if (token == null) return this.StatusCode(403, ""); if (token == null) return this.StatusCode(403, "");
@ -78,6 +79,7 @@ public class ScoreController : ControllerBase
if (slotType == "developer") id = await SlotHelper.GetPlaceholderSlotId(this.database, id, SlotType.Developer); if (slotType == "developer") id = await SlotHelper.GetPlaceholderSlotId(this.database, id, SlotType.Developer);
score.SlotId = id; score.SlotId = id;
score.ChildSlotId = childId;
Slot? slot = this.database.Slots.FirstOrDefault(s => s.SlotId == score.SlotId); Slot? slot = this.database.Slots.FirstOrDefault(s => s.SlotId == score.SlotId);
if (slot == null) if (slot == null)
@ -116,12 +118,13 @@ public class ScoreController : ControllerBase
Type = score.Type, Type = score.Type,
Points = score.Points, Points = score.Points,
SlotId = score.SlotId, SlotId = score.SlotId,
ChildSlotId = score.ChildSlotId
}; };
IQueryable<Score> existingScore = this.database.Scores.Where(s => s.SlotId == playerScore.SlotId) IQueryable<Score> existingScore = this.database.Scores.Where(s => s.SlotId == playerScore.SlotId)
.Where(s => childId != 0 || s.ChildSlotId == childId)
.Where(s => s.PlayerIdCollection == playerScore.PlayerIdCollection) .Where(s => s.PlayerIdCollection == playerScore.PlayerIdCollection)
.Where(s => s.Type == playerScore.Type); .Where(s => s.Type == playerScore.Type);
if (existingScore.Any()) if (existingScore.Any())
{ {
Score first = existingScore.First(s => s.SlotId == playerScore.SlotId); Score first = existingScore.First(s => s.SlotId == playerScore.SlotId);
@ -137,13 +140,14 @@ public class ScoreController : ControllerBase
await this.database.SaveChangesAsync(); await this.database.SaveChangesAsync();
string myRanking = this.getScores(score.SlotId, score.Type, username, -1, 5, "scoreboardSegment"); string myRanking = this.getScores(score.SlotId, score.Type, username, -1, 5, "scoreboardSegment", childId: score.ChildSlotId);
return this.Ok(myRanking); return this.Ok(myRanking);
} }
[HttpGet("friendscores/{slotType}/{slotId:int}/{type:int}")] [HttpGet("friendscores/{slotType}/{slotId:int}/{type:int}")]
public async Task<IActionResult> FriendScores(string slotType, int slotId, int type, [FromQuery] int pageStart = -1, [FromQuery] int pageSize = 5) [HttpGet("friendscores/{slotType}/{slotId:int}/{childId:int}/{type:int}")]
public async Task<IActionResult> FriendScores(string slotType, int slotId, int? childId, int type, [FromQuery] int pageStart = -1, [FromQuery] int pageSize = 5)
{ {
GameToken? token = await this.database.GameTokenFromRequest(this.Request); GameToken? token = await this.database.GameTokenFromRequest(this.Request);
if (token == null) return this.StatusCode(403, ""); if (token == null) return this.StatusCode(403, "");
@ -169,12 +173,13 @@ public class ScoreController : ControllerBase
if (friendUsername != null) friendNames.Add(friendUsername); if (friendUsername != null) friendNames.Add(friendUsername);
} }
return this.Ok(this.getScores(slotId, type, username, pageStart, pageSize, "scores", friendNames.ToArray())); return this.Ok(this.getScores(slotId, type, username, pageStart, pageSize, "scores", friendNames.ToArray(), childId));
} }
[HttpGet("topscores/{slotType}/{slotId:int}/{type:int}")] [HttpGet("topscores/{slotType}/{slotId:int}/{type:int}")]
[HttpGet("topscores/{slotType}/{slotId:int}/{childId:int}/{type:int}")]
[SuppressMessage("ReSharper", "PossibleMultipleEnumeration")] [SuppressMessage("ReSharper", "PossibleMultipleEnumeration")]
public async Task<IActionResult> TopScores(string slotType, int slotId, int type, [FromQuery] int pageStart = -1, [FromQuery] int pageSize = 5) public async Task<IActionResult> TopScores(string slotType, int slotId, int? childId, int type, [FromQuery] int pageStart = -1, [FromQuery] int pageSize = 5)
{ {
GameToken? token = await this.database.GameTokenFromRequest(this.Request); GameToken? token = await this.database.GameTokenFromRequest(this.Request);
if (token == null) return this.StatusCode(403, ""); if (token == null) return this.StatusCode(403, "");
@ -187,7 +192,7 @@ public class ScoreController : ControllerBase
if (slotType == "developer") slotId = await SlotHelper.GetPlaceholderSlotId(this.database, slotId, SlotType.Developer); if (slotType == "developer") slotId = await SlotHelper.GetPlaceholderSlotId(this.database, slotId, SlotType.Developer);
return this.Ok(this.getScores(slotId, type, username, pageStart, pageSize)); return this.Ok(this.getScores(slotId, type, username, pageStart, pageSize, childId: childId));
} }
[SuppressMessage("ReSharper", "PossibleMultipleEnumeration")] [SuppressMessage("ReSharper", "PossibleMultipleEnumeration")]
@ -199,7 +204,8 @@ public class ScoreController : ControllerBase
int pageStart = -1, int pageStart = -1,
int pageSize = 5, int pageSize = 5,
string rootName = "scores", string rootName = "scores",
string[]? playerIds = null string[]? playerIds = null,
int? childId = 0
) )
{ {
@ -207,8 +213,9 @@ public class ScoreController : ControllerBase
// var needed for Anonymous type returned from SELECT // var needed for Anonymous type returned from SELECT
var rankedScores = this.database.Scores var rankedScores = this.database.Scores
.Where(s => s.SlotId == slotId && s.Type == type) .Where(s => s.SlotId == slotId && s.Type == type)
.AsEnumerable() .Where(s => s.ChildSlotId == null || s.ChildSlotId == childId)
.Where(s => playerIds == null || playerIds.Any(id => s.PlayerIdCollection.Contains(id))) .Where(s => playerIds == null || playerIds.Any(id => s.PlayerIdCollection.Contains(id)))
.AsEnumerable()
.OrderByDescending(s => s.Points) .OrderByDescending(s => s.Points)
.ThenBy(s => s.ScoreId) .ThenBy(s => s.ScoreId)
.ToList() .ToList()

View file

@ -37,7 +37,8 @@
{ {
case SlotType.User: case SlotType.User:
<span> <span>
in level <b><a href="/slot/@Model.SlotId">@HttpUtility.HtmlDecode(Model.Slot.Name)</a></b> @(Model.Slot.IsAdventurePlanet ? "on an adventure in" : "in level")
<b><a href="/slot/@Model.SlotId">@HttpUtility.HtmlDecode(Model.Slot.Name)</a></b>
</span> </span>
break; break;
case SlotType.Developer: case SlotType.Developer:

View file

@ -39,11 +39,13 @@
<div class="card"> <div class="card">
@{ @{
int size = isMobile || mini ? 50 : 100; int size = isMobile || mini ? 50 : 100;
bool isAdventure = Model.IsAdventurePlanet;
string advenStyleExt = isAdventure ? "-webkit-mask-image: url(/assets/advSlotCardMask.png); -webkit-mask-size: contain; border-radius: 0%;" : "";
} }
<div> <div>
<img src="~/assets/slotCardOverlay.png" style="min-width: @(size)px; width: @(size)px; height: @(size)px; pointer-events: none; position: absolute; z-index: 3"> <img src=@(isAdventure ? "/assets/advSlotCardOverlay.png" : "/assets/slotCardOverlay.png") style="min-width: @(size)px; width: @(size)px; height: @(size)px; pointer-events: none; position: absolute; z-index: 3;">
<img src="~/assets/slotCardBackground.png" style="min-width: @(size)px; width: @(size)px; height: @(size)px; position: absolute; z-index: 1;"> <img src="~/assets/slotCardBackground.png" style="min-width: @(size)px; width: @(size)px; height: @(size)px; position: absolute; z-index: 1; @(advenStyleExt)">
<img class="cardIcon slotCardIcon" src="/gameAssets/@iconHash" style="min-width: @(size)px; width: @(size)px; height: @(size)px; position: relative; z-index: 2" <img class="cardIcon slotCardIcon" src="/gameAssets/@iconHash" style="min-width: @(size)px; width: @(size)px; height: @(size)px; position: relative; z-index: 2; @(advenStyleExt)"
onerror="this.onerror='';this.src='/gameAssets/@ServerConfiguration.Instance.WebsiteConfiguration.MissingIconHash'"> onerror="this.onerror='';this.src='/gameAssets/@ServerConfiguration.Instance.WebsiteConfiguration.MissingIconHash'">
</div> </div>
<div class="cardStats"> <div class="cardStats">

View file

@ -64,6 +64,7 @@ public static class FileHelper
LbpFileType.Texture => true, LbpFileType.Texture => true,
LbpFileType.Script => false, LbpFileType.Script => false,
LbpFileType.Level => true, LbpFileType.Level => true,
LbpFileType.Adventure => true,
LbpFileType.Voice => true, LbpFileType.Voice => true,
LbpFileType.Quest => true, LbpFileType.Quest => true,
LbpFileType.Plan => true, LbpFileType.Plan => true,
@ -190,6 +191,8 @@ public static class FileHelper
"FSHb" => LbpFileType.Script, "FSHb" => LbpFileType.Script,
"VOPb" => LbpFileType.Voice, "VOPb" => LbpFileType.Voice,
"LVLb" => LbpFileType.Level, "LVLb" => LbpFileType.Level,
"ADCb" => LbpFileType.Adventure,
"ADSb" => LbpFileType.Adventure,
"PLNb" => LbpFileType.Plan, "PLNb" => LbpFileType.Plan,
"QSTb" => LbpFileType.Quest, "QSTb" => LbpFileType.Quest,
_ => readAlternateHeader(reader), _ => readAlternateHeader(reader),

View file

@ -5,6 +5,7 @@ public enum LbpFileType
Script, // .ff, FSH Script, // .ff, FSH
Texture, // TEX Texture, // TEX
Level, // LVL Level, // LVL
Adventure, // ADC, ADS
CrossLevel, // PRF, Cross controller level CrossLevel, // PRF, Cross controller level
FileArchive, // .farc, (ends with FARC) FileArchive, // .farc, (ends with FARC)
Plan, // PLN, uploaded with levels Plan, // PLN, uploaded with levels

View file

@ -60,6 +60,9 @@ public class Slot
[XmlElement("icon")] [XmlElement("icon")]
public string IconHash { get; set; } = ""; public string IconHash { get; set; } = "";
[XmlElement("isAdventurePlanet")]
public bool IsAdventurePlanet { get; set; }
[XmlElement("rootLevel")] [XmlElement("rootLevel")]
[JsonIgnore] [JsonIgnore]
public string RootLevel { get; set; } = ""; public string RootLevel { get; set; } = "";
@ -301,6 +304,7 @@ public class Slot
LbpSerializer.StringElement("initiallyLocked", this.InitiallyLocked) + LbpSerializer.StringElement("initiallyLocked", this.InitiallyLocked) +
LbpSerializer.StringElement("isSubLevel", this.SubLevel) + LbpSerializer.StringElement("isSubLevel", this.SubLevel) +
LbpSerializer.StringElement("isLBP1Only", this.Lbp1Only) + LbpSerializer.StringElement("isLBP1Only", this.Lbp1Only) +
LbpSerializer.StringElement("isAdventurePlanet", this.IsAdventurePlanet) +
LbpSerializer.StringElement("background", this.BackgroundHash) + LbpSerializer.StringElement("background", this.BackgroundHash) +
LbpSerializer.StringElement("shareable", this.Shareable) + LbpSerializer.StringElement("shareable", this.Shareable) +
LbpSerializer.StringElement("authorLabels", this.AuthorLabels) + LbpSerializer.StringElement("authorLabels", this.AuthorLabels) +

View file

@ -0,0 +1,23 @@
using LBPUnion.ProjectLighthouse;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ProjectLighthouse.Migrations
{
[DbContext(typeof(Database))]
[Migration("20220916141401_ScoreboardAdvSlot")]
public partial class CreateScoreboardAdvSlot : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "ChildSlotId",
table: "Scores",
type: "int",
nullable: false,
defaultValue: 0);
}
}
}

View file

@ -0,0 +1,23 @@
using LBPUnion.ProjectLighthouse;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ProjectLighthouse.Migrations
{
[DbContext(typeof(Database))]
[Migration("20220918154500_AddIsAdventureColumn")]
public partial class AddisAdventureColumn : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<long>(
name: "IsAdventurePlanet",
table: "Slots",
type: "bool",
nullable: false,
defaultValue: false);
}
}
}

View file

@ -21,6 +21,9 @@ public class Score
[ForeignKey(nameof(SlotId))] [ForeignKey(nameof(SlotId))]
public Slot Slot { get; set; } public Slot Slot { get; set; }
[XmlIgnore]
public int? ChildSlotId { get; set; }
[XmlElement("type")] [XmlElement("type")]
public int Type { get; set; } public int Type { get; set; }

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB