Skip to content
Merged
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
44 changes: 44 additions & 0 deletions Refresh.Database/GameDatabaseContext.LevelRevisions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using Refresh.Database.Models.Levels;
using Refresh.Database.Models.Users;

namespace Refresh.Database;

public partial class GameDatabaseContext // LevelRevisions
{
private IQueryable<GameLevelRevision> GameLevelRevisionsIncluded => this.GameLevelRevisions
.Include(r => r.Level);

public GameLevelRevision CreateRevisionForLevel(GameLevel level, GameUser? creator, bool saveChanges = false)
{
// FIXME: this isn't exactly atomic, but it should be incredibly rare
// for multiple threads to be trying to make a revision for the same level at the same time
int sequentialId = this.GameLevelRevisions
.Where(r => r.LevelId == level.LevelId)
.DefaultIfEmpty()
.Max(r => r != null ? r.RevisionId : 0);

GameLevelRevision revision = new()
{
LevelId = level.LevelId,
CreatedAt = this._time.Now,
CreatedById = creator?.UserId,

RevisionId = sequentialId + 1,

Title = level.Title,
Description = level.Description,
GameVersion = level.GameVersion,
IconHash = level.IconHash,
RootResource = level.RootResource,
LevelType = level.LevelType,
StoryId = level.StoryId,
};

this.GameLevelRevisions.Add(revision);

if(saveChanges)
this.SaveChanges();

return revision;
}
}
61 changes: 29 additions & 32 deletions Refresh.Database/GameDatabaseContext.Levels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,19 +55,18 @@ public bool AddLevel(GameLevel level)
level.OriginalPublisher = levelAttributes.GetValueOrDefault("op") ?? SystemUsers.UnknownUserName;
}

this.Write(() =>
{
this.GameLevels.Add(level);
});

this.Write(() =>
this.GameLevels.Add(level);

this.SaveChanges();

this.CreateRevisionForLevel(level, level.Publisher);
this.GameLevelStatistics.Add(level.Statistics = new GameLevelStatistics
{
this.GameLevelStatistics.Add(level.Statistics = new GameLevelStatistics()
{
LevelId = level.LevelId,
});
LevelId = level.LevelId,
});

this.SaveChanges();

if (level.Publisher != null)
{
this.WriteEnsuringStatistics(level.Publisher, () =>
Expand Down Expand Up @@ -206,46 +205,44 @@ public void UpdateLevelLocations(IEnumerable<ISerializedEditLevelLocation> locat
// Now newLevel is set up to replace oldLevel.
// If information is lost here, then that's probably a bug.
// Update the level's properties in the database
this.Write(() =>
PropertyInfo[] userProps = typeof(GameLevel).GetProperties();
foreach (PropertyInfo prop in userProps)
{
PropertyInfo[] userProps = typeof(GameLevel).GetProperties();
foreach (PropertyInfo prop in userProps)
{
if (!prop.CanWrite || !prop.CanRead) continue;
prop.SetValue(oldLevel, prop.GetValue(newLevel));
}
});
if (!prop.CanWrite || !prop.CanRead) continue;
prop.SetValue(oldLevel, prop.GetValue(newLevel));
}

this.CreateRevisionForLevel(newLevel, author);
this.SaveChanges();
return oldLevel;
}

public GameLevel? UpdateLevel(IApiEditLevelRequest body, GameLevel level)
public GameLevel? UpdateLevel(IApiEditLevelRequest body, GameLevel level, GameUser? updatingUser)
{
if (body.Title is { Length: > UgcLimits.TitleLimit })
body.Title = body.Title[..UgcLimits.TitleLimit];

if (body.Description is { Length: > UgcLimits.DescriptionLimit })
body.Description = body.Description[..UgcLimits.DescriptionLimit];

this.Write(() =>
PropertyInfo[] userProps = body.GetType().GetProperties();
foreach (PropertyInfo prop in userProps)
{
PropertyInfo[] userProps = body.GetType().GetProperties();
foreach (PropertyInfo prop in userProps)
{
if (!prop.CanWrite || !prop.CanRead) continue;
if (!prop.CanWrite || !prop.CanRead) continue;

object? propValue = prop.GetValue(body);
if(propValue == null) continue;
object? propValue = prop.GetValue(body);
if(propValue == null) continue;

PropertyInfo? gameLevelProp = level.GetType().GetProperty(prop.Name);
Debug.Assert(gameLevelProp != null, $"Invalid property {prop.Name} on {nameof(IApiEditLevelRequest)}");
PropertyInfo? gameLevelProp = level.GetType().GetProperty(prop.Name);
Debug.Assert(gameLevelProp != null, $"Invalid property {prop.Name} on {nameof(IApiEditLevelRequest)}");

gameLevelProp.SetValue(level, prop.GetValue(body));
}
gameLevelProp.SetValue(level, prop.GetValue(body));
}

level.UpdateDate = this._time.Now;
});
level.UpdateDate = this._time.Now;

this.CreateRevisionForLevel(level, updatingUser);
this.SaveChanges();
return level;
}

Expand Down
1 change: 1 addition & 0 deletions Refresh.Database/GameDatabaseContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ public partial class GameDatabaseContext : DbContext, IDatabaseContext
internal DbSet<GameSkillReward> GameSkillRewards { get; set; }
internal DbSet<WorkerInfo> Workers { get; set; }
internal DbSet<PersistentJobState> JobStates { get; set; }
internal DbSet<GameLevelRevision> GameLevelRevisions { get; set; }

#pragma warning disable CS8618 // Non-nullable variable must contain a non-null value when exiting constructor. Consider declaring it as nullable.
internal GameDatabaseContext(Logger logger, IDateTimeProvider time, IDatabaseConfig dbConfig)
Expand Down
63 changes: 63 additions & 0 deletions Refresh.Database/Migrations/20250721211144_AddRevisionTable.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using System;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

namespace Refresh.Database.Migrations
{
[DbContext(typeof(GameDatabaseContext))]
[Migration("20250721211144_AddRevisionTable")]
/// <inheritdoc />
public partial class AddRevisionTable : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "GameLevelRevisions",
columns: table => new
{
LevelId = table.Column<int>(type: "integer", nullable: false),
RevisionId = table.Column<int>(type: "integer", nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
CreatedById = table.Column<string>(type: "text", nullable: false),
Title = table.Column<string>(type: "text", nullable: false),
IconHash = table.Column<string>(type: "text", nullable: false),
Description = table.Column<string>(type: "text", nullable: false),
RootResource = table.Column<string>(type: "text", nullable: false),
GameVersion = table.Column<int>(type: "integer", nullable: false),
LevelType = table.Column<byte>(type: "smallint", nullable: false),
StoryId = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_GameLevelRevisions", x => new { x.LevelId, x.RevisionId });
table.ForeignKey(
name: "FK_GameLevelRevisions_GameLevels_LevelId",
column: x => x.LevelId,
principalTable: "GameLevels",
principalColumn: "LevelId",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_GameLevelRevisions_GameUsers_CreatedById",
column: x => x.CreatedById,
principalTable: "GameUsers",
principalColumn: "UserId",
onDelete: ReferentialAction.Cascade);
});

migrationBuilder.CreateIndex(
name: "IX_GameLevelRevisions_CreatedById",
table: "GameLevelRevisions",
column: "CreatedById");
}

/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "GameLevelRevisions");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

namespace Refresh.Database.Migrations
{
[DbContext(typeof(GameDatabaseContext))]
[Migration("20250723050220_MakeRevisionCreatorUserIdNullable")]
/// <inheritdoc />
public partial class MakeRevisionCreatorUserIdNullable : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_GameLevelRevisions_GameUsers_CreatedById",
table: "GameLevelRevisions");

migrationBuilder.AlterColumn<string>(
name: "CreatedById",
table: "GameLevelRevisions",
type: "text",
nullable: true,
oldClrType: typeof(string),
oldType: "text");

migrationBuilder.AddForeignKey(
name: "FK_GameLevelRevisions_GameUsers_CreatedById",
table: "GameLevelRevisions",
column: "CreatedById",
principalTable: "GameUsers",
principalColumn: "UserId");
}

/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_GameLevelRevisions_GameUsers_CreatedById",
table: "GameLevelRevisions");

migrationBuilder.AlterColumn<string>(
name: "CreatedById",
table: "GameLevelRevisions",
type: "text",
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "text",
oldNullable: true);

migrationBuilder.AddForeignKey(
name: "FK_GameLevelRevisions_GameUsers_CreatedById",
table: "GameLevelRevisions",
column: "CreatedById",
principalTable: "GameUsers",
principalColumn: "UserId",
onDelete: ReferentialAction.Cascade);
}
}
}
48 changes: 48 additions & 0 deletions Refresh.Database/Migrations/20250723060405_SwapRevisionKeyOrder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

namespace Refresh.Database.Migrations
{
[DbContext(typeof(GameDatabaseContext))]
[Migration("20250723060405_SwapRevisionKeyOrder")]
/// <inheritdoc />
public partial class SwapRevisionKeyOrder : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropPrimaryKey(
name: "PK_GameLevelRevisions",
table: "GameLevelRevisions");

migrationBuilder.AddPrimaryKey(
name: "PK_GameLevelRevisions",
table: "GameLevelRevisions",
columns: new[] { "RevisionId", "LevelId" });

migrationBuilder.CreateIndex(
name: "IX_GameLevelRevisions_LevelId",
table: "GameLevelRevisions",
column: "LevelId");
}

/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropPrimaryKey(
name: "PK_GameLevelRevisions",
table: "GameLevelRevisions");

migrationBuilder.DropIndex(
name: "IX_GameLevelRevisions_LevelId",
table: "GameLevelRevisions");

migrationBuilder.AddPrimaryKey(
name: "PK_GameLevelRevisions",
table: "GameLevelRevisions",
columns: new[] { "LevelId", "RevisionId" });
}
}
}
Loading