Added sort ordering to actions and fixed install process not updating game from live manifest

dashboard
Pat Hartl 2023-01-16 23:45:46 -06:00
parent 09df7a8997
commit abecd9f2f2
13 changed files with 1325 additions and 111 deletions

View File

@ -15,6 +15,7 @@ using ICSharpCode.SharpZipLib.Core;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
using LANCommander.SDK.Models;
using System.Collections.ObjectModel;
namespace LANCommander.PlaynitePlugin
{
@ -37,6 +38,7 @@ namespace LANCommander.PlaynitePlugin
var gameId = Guid.Parse(Game.GameId);
var game = Plugin.LANCommander.GetGame(gameId);
var manifest = Plugin.LANCommander.GetGameManifest(gameId);
var tempFile = Download(game);
@ -49,7 +51,7 @@ namespace LANCommander.PlaynitePlugin
PlayniteGame.InstallDirectory = installDirectory;
File.WriteAllText(Path.Combine(installDirectory, "_manifest.yml"), GetManifest(gameId));
WriteManifest(manifest, installDirectory);
SaveScript(game, installDirectory, ScriptType.Install);
SaveScript(game, installDirectory, ScriptType.Uninstall);
@ -62,9 +64,9 @@ namespace LANCommander.PlaynitePlugin
}
catch { }
InvokeOnInstalled(new GameInstalledEventArgs(installInfo));
Plugin.UpdateGame(manifest, gameId);
Plugin.UpdateGamesFromManifest();
InvokeOnInstalled(new GameInstalledEventArgs(installInfo));
}
private string Download(LANCommander.SDK.Models.Game game)
@ -164,17 +166,15 @@ namespace LANCommander.PlaynitePlugin
return destination;
}
private string GetManifest(Guid gameId)
private void WriteManifest(SDK.GameManifest manifest, string installDirectory)
{
var manifest = Plugin.LANCommander.GetGameManifest(gameId);
var serializer = new SerializerBuilder()
.WithNamingConvention(PascalCaseNamingConvention.Instance)
.Build();
var yaml = serializer.Serialize(manifest);
return yaml;
File.WriteAllText(Path.Combine(installDirectory, "_manifest.yml"), yaml);
}
private void SaveScript(LANCommander.SDK.Models.Game game, string installationDirectory, ScriptType type)

View File

@ -5,6 +5,7 @@ using Playnite.SDK.Models;
using Playnite.SDK.Plugins;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Net.NetworkInformation;
@ -94,7 +95,7 @@ namespace LANCommander.PlaynitePlugin
ReleaseDate = new ReleaseDate(manifest.ReleasedOn),
//Version = game.Archives.OrderByDescending(a => a.CreatedOn).FirstOrDefault().Version,
Icon = new MetadataFile(iconUri.ToString()),
GameActions = game.Actions.Select(a => new PN.SDK.Models.GameAction()
GameActions = game.Actions.OrderBy(a => a.SortOrder).Select(a => new PN.SDK.Models.GameAction()
{
Name = a.Name,
Arguments = a.Arguments,
@ -139,7 +140,25 @@ namespace LANCommander.PlaynitePlugin
metadata.Features.Add(new MetadataNameProperty($"Online Multiplayer {manifest.OnlineMultiplayer.GetPlayerCount()}".Trim()));
gameMetadata.Add(metadata);
if (existingGame != null)
{
existingGame.GameActions.Clear();
foreach (var action in game.Actions)
{
existingGame.GameActions.Add(new PN.SDK.Models.GameAction()
{
Name = action.Name,
Arguments = action.Arguments,
Path = action.Path,
WorkingDir = action.WorkingDirectory,
IsPlayAction = action.PrimaryAction
});
}
}
};
}
catch (Exception ex)
{
@ -272,113 +291,83 @@ namespace LANCommander.PlaynitePlugin
return window;
}
public void UpdateGamesFromManifest()
public void UpdateGame(SDK.GameManifest manifest, Guid gameId)
{
var games = PlayniteApi.Database.Games;
var game = PlayniteApi.Database.Games.First(g => g.GameId == gameId.ToString());
foreach (var game in games.Where(g => g.PluginId == Id && g.IsInstalled))
if (game.GameActions == null)
game.GameActions = new ObservableCollection<PN.SDK.Models.GameAction>();
else
game.GameActions.Clear();
foreach (var action in manifest.Actions.OrderBy(a => a.SortOrder))
{
if (!Directory.Exists(game.InstallDirectory))
continue;
bool isFirstAction = !manifest.Actions.Any(a => a.IsPrimaryAction) && manifest.Actions.First().Name == action.Name;
var manifestPath = Path.Combine(game.InstallDirectory, "_manifest.yml");
if (File.Exists(manifestPath))
game.GameActions.Add(new PN.SDK.Models.GameAction()
{
try
Name = action.Name,
Arguments = action.Arguments,
Path = PlayniteApi.ExpandGameVariables(game, action.Path?.Replace('/', Path.DirectorySeparatorChar)),
WorkingDir = action.WorkingDirectory?.Replace('/', Path.DirectorySeparatorChar) ?? game.InstallDirectory,
IsPlayAction = action.IsPrimaryAction || isFirstAction
});
}
#region Features
var singlePlayerFeature = PlayniteApi.Database.Features.FirstOrDefault(f => f.Name == "Single Player");
if (manifest.LanMultiplayer != null)
{
var multiplayerInfo = manifest.LanMultiplayer;
string playerCount = multiplayerInfo.MinPlayers == multiplayerInfo.MaxPlayers ? $"({multiplayerInfo.MinPlayers} players)" : $"({multiplayerInfo.MinPlayers} - {multiplayerInfo.MaxPlayers} players)";
string featureName = $"LAN Multiplayer {playerCount}";
if (PlayniteApi.Database.Features.Any(f => f.Name == featureName))
{
game.Features.Add(PlayniteApi.Database.Features.FirstOrDefault(f => f.Name == featureName));
}
else
{
PlayniteApi.Database.Features.Add(new GameFeature()
{
var manifestContents = File.ReadAllText(manifestPath);
var deserializer = new DeserializerBuilder()
.IgnoreUnmatchedProperties()
.WithNamingConvention(PascalCaseNamingConvention.Instance)
.Build();
Name = featureName
});
var manifest = deserializer.Deserialize<GameManifest>(manifestContents);
#region Actions
if (game.GameActions == null)
game.GameActions = new System.Collections.ObjectModel.ObservableCollection<PN.SDK.Models.GameAction>();
foreach (var action in manifest.Actions)
{
bool isFirstAction = !manifest.Actions.Any(a => a.IsPrimaryAction) && manifest.Actions.First().Name == action.Name;
foreach (var existingAction in game.GameActions)
if (action.Name == existingAction.Name)
game.GameActions.Remove(existingAction);
game.GameActions.AddMissing(new PN.SDK.Models.GameAction()
{
Name = action.Name,
Arguments = action.Arguments,
Path = PlayniteApi.ExpandGameVariables(game, action.Path?.Replace('/', Path.DirectorySeparatorChar)),
WorkingDir = action.WorkingDirectory?.Replace('/', Path.DirectorySeparatorChar) ?? game.InstallDirectory,
IsPlayAction = action.IsPrimaryAction || isFirstAction
});
}
#endregion
#region Features
var singlePlayerFeature = PlayniteApi.Database.Features.FirstOrDefault(f => f.Name == "Single Player");
if (manifest.LanMultiplayer != null)
{
var multiplayerInfo = manifest.LanMultiplayer;
string playerCount = multiplayerInfo.MinPlayers == multiplayerInfo.MaxPlayers ? $"({multiplayerInfo.MinPlayers} players)" : $"({multiplayerInfo.MinPlayers} - {multiplayerInfo.MaxPlayers} players)";
string featureName = $"LAN Multiplayer {playerCount}";
if (PlayniteApi.Database.Features.Any(f => f.Name == featureName))
{
game.Features.Add(PlayniteApi.Database.Features.FirstOrDefault(f => f.Name == featureName));
}
else
{
PlayniteApi.Database.Features.Add(new PN.SDK.Models.GameFeature()
{
Name = featureName
});
game.Features.Add(new PN.SDK.Models.GameFeature()
{
Name = $"LAN Multiplayer {playerCount}"
});
}
}
if (manifest.LocalMultiplayer != null)
{
var multiplayerInfo = manifest.LocalMultiplayer;
string playerCount = multiplayerInfo.MinPlayers == multiplayerInfo.MaxPlayers ? $"({multiplayerInfo.MinPlayers} players)" : $"({multiplayerInfo.MinPlayers} - {multiplayerInfo.MaxPlayers} players)";
game.Features.Add(new PN.SDK.Models.GameFeature()
{
Name = $"Local Multiplayer {playerCount}"
});
}
if (manifest.OnlineMultiplayer != null)
{
var multiplayerInfo = manifest.OnlineMultiplayer;
string playerCount = multiplayerInfo.MinPlayers == multiplayerInfo.MaxPlayers ? $"({multiplayerInfo.MinPlayers} players)" : $"({multiplayerInfo.MinPlayers} - {multiplayerInfo.MaxPlayers} players)";
game.Features.Add(new PN.SDK.Models.GameFeature()
{
Name = $"Online Multiplayer {playerCount}"
});
}
#endregion
PlayniteApi.Database.Games.Update(game);
}
catch (Exception ex)
game.Features.Add(new GameFeature()
{
}
Name = $"LAN Multiplayer {playerCount}"
});
}
}
if (manifest.LocalMultiplayer != null)
{
var multiplayerInfo = manifest.LocalMultiplayer;
string playerCount = multiplayerInfo.MinPlayers == multiplayerInfo.MaxPlayers ? $"({multiplayerInfo.MinPlayers} players)" : $"({multiplayerInfo.MinPlayers} - {multiplayerInfo.MaxPlayers} players)";
game.Features.Add(new GameFeature()
{
Name = $"Local Multiplayer {playerCount}"
});
}
if (manifest.OnlineMultiplayer != null)
{
var multiplayerInfo = manifest.OnlineMultiplayer;
string playerCount = multiplayerInfo.MinPlayers == multiplayerInfo.MaxPlayers ? $"({multiplayerInfo.MinPlayers} players)" : $"({multiplayerInfo.MinPlayers} - {multiplayerInfo.MaxPlayers} players)";
game.Features.Add(new GameFeature()
{
Name = $"Online Multiplayer {playerCount}"
});
}
#endregion
PlayniteApi.Database.Games.Update(game);
}
}
}

View File

@ -11,5 +11,6 @@ namespace LANCommander.SDK.Models
public string Path { get; set; }
public string WorkingDirectory { get; set; }
public bool PrimaryAction { get; set; }
public int SortOrder { get; set; }
}
}

View File

@ -31,6 +31,7 @@ namespace LANCommander.SDK
public string Path { get; set; }
public string WorkingDirectory { get; set; }
public bool IsPrimaryAction { get; set; }
public int SortOrder { get; set; }
}
public class MultiplayerInfo

View File

@ -1,4 +1,5 @@
@using LANCommander.Data.Models
@using LANCommander.Extensions
@{
int i = 0;
@ -21,7 +22,7 @@
<tr><td colspan="5">Actions are used to start the game or launch other executables. It is recommended to have at least one action to launch the game.</td></tr>
}
@foreach (var action in Actions)
@foreach (var action in Actions.OrderBy(a => a.SortOrder))
{
var index = i;
@ -37,8 +38,23 @@
<td>
<input name="Game.Actions[@i].Id" type="hidden" value="@action.Id" />
<input name="Game.Actions[@i].GameId" type="hidden" value="@GameId" />
<input name="Game.Actions[@i].SortOrder" type="hidden" value="@i" />
<div class="btn-list flex-nowrap justify-content-end">
<button class="btn btn-ghost-secondary btn-icon" @onclick="() => MoveUp(index)" type="button">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-chevron-up" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<polyline points="6 15 12 9 18 15"></polyline>
</svg>
</button>
<button class="btn btn-ghost-secondary btn-icon" @onclick="() => MoveDown(index)" type="button">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-chevron-down" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</button>
<button class="btn btn-ghost-danger btn-icon" @onclick="() => RemoveAction(index)" type="button">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-x" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
@ -55,6 +71,7 @@
<tr>
<td colspan="5">
<div class="btn-list flex-nowrap justify-content-end">
<button class="btn btn-ghost-primary" @onclick="AddAction" type="button">Add Action</button>
</div>
</td>
@ -64,9 +81,18 @@
</div>
@code {
[Parameter] public ICollection<Data.Models.Action> Actions { get; set; }
[Parameter] public List<Data.Models.Action> Actions { get; set; }
[Parameter] public Guid GameId { get; set; }
protected override void OnInitialized()
{
Actions = Actions.OrderBy(a => a.SortOrder).ToList();
FixSortOrders();
base.OnInitialized();
}
private void AddAction()
{
if (Actions == null)
@ -74,12 +100,40 @@
Actions.Add(new Data.Models.Action()
{
PrimaryAction = Actions.Count == 0
PrimaryAction = Actions.Count == 0,
SortOrder = Actions.Count
});
}
private void RemoveAction(int index)
{
Actions.Remove(Actions.ElementAt(index));
FixSortOrders();
}
private void MoveUp(int index)
{
if (index == 0)
return;
Actions.Move(Actions.ElementAt(index), index - 1);
FixSortOrders();
}
private void MoveDown(int index)
{
if (index == Actions.Count - 1)
return;
Actions.Move(Actions.ElementAt(index), index + 1);
FixSortOrders();
}
private void FixSortOrders() {
for (int i = 0; i < Actions.Count; i++)
{
Actions.ElementAt(i).SortOrder = i;
}
}
}

View File

@ -11,6 +11,7 @@ namespace LANCommander.Data.Models
public string? Path { get; set; }
public string? WorkingDirectory { get; set; }
public bool PrimaryAction { get; set; }
public int SortOrder { get; set; }
public Guid GameId { get; set; }
[JsonIgnore]

View File

@ -0,0 +1,36 @@
namespace LANCommander.Extensions
{
public static class ListExtensions
{
public static void Move<T>(this List<T> list, int oldIndex, int newIndex)
{
var item = list[oldIndex];
list.RemoveAt(oldIndex);
if (newIndex > oldIndex) newIndex--;
// the actual index could have shifted due to the removal
list.Insert(newIndex, item);
}
public static void Move<T>(this List<T> list, T item, int newIndex)
{
if (item != null)
{
var oldIndex = list.IndexOf(item);
if (oldIndex > -1)
{
list.RemoveAt(oldIndex);
if (newIndex > oldIndex) newIndex--;
// the actual index could have shifted due to the removal
list.Insert(newIndex, item);
}
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,26 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace LANCommander.Migrations
{
public partial class AddActionSortOrder : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "SortOrder",
table: "Actions",
type: "INTEGER",
nullable: false,
defaultValue: 0);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "SortOrder",
table: "Actions");
}
}
}

View File

@ -120,6 +120,9 @@ namespace LANCommander.Migrations
b.Property<bool>("PrimaryAction")
.HasColumnType("INTEGER");
b.Property<int>("SortOrder")
.HasColumnType("INTEGER");
b.Property<Guid?>("UpdatedById")
.HasColumnType("TEXT");

View File

@ -71,7 +71,8 @@ namespace LANCommander.Services
Arguments = a.Arguments,
Path = a.Path,
WorkingDirectory = a.WorkingDirectory,
IsPrimaryAction = a.PrimaryAction
IsPrimaryAction = a.PrimaryAction,
SortOrder = a.SortOrder,
}).ToArray();
}

View File

@ -93,7 +93,7 @@
<h3 class="card-title">Actions</h3>
</div>
<component type="typeof(ActionEditor)" render-mode="Server" param-Actions="Model.Game.Actions" param-GameId="Model.Game.Id" />
<component type="typeof(ActionEditor)" render-mode="Server" param-Actions="Model.Game.Actions.ToList()" param-GameId="Model.Game.Id" />
<div class="card-header">
<h3 class="card-title">Multiplayer Modes</h3>

View File

@ -96,7 +96,7 @@
<h3 class="card-title">Actions</h3>
</div>
<component type="typeof(ActionEditor)" render-mode="Server" param-Actions="Model.Game.Actions" param-GameId="Model.Game.Id" />
<component type="typeof(ActionEditor)" render-mode="Server" param-Actions="Model.Game.Actions.ToList()" param-GameId="Model.Game.Id" />
<div class="card-header">
<h3 class="card-title">Multiplayer Modes</h3>