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;
using YamlDotNet.Serialization.NamingConventions; using YamlDotNet.Serialization.NamingConventions;
using LANCommander.SDK.Models; using LANCommander.SDK.Models;
using System.Collections.ObjectModel;
namespace LANCommander.PlaynitePlugin namespace LANCommander.PlaynitePlugin
{ {
@ -37,6 +38,7 @@ namespace LANCommander.PlaynitePlugin
var gameId = Guid.Parse(Game.GameId); var gameId = Guid.Parse(Game.GameId);
var game = Plugin.LANCommander.GetGame(gameId); var game = Plugin.LANCommander.GetGame(gameId);
var manifest = Plugin.LANCommander.GetGameManifest(gameId);
var tempFile = Download(game); var tempFile = Download(game);
@ -49,7 +51,7 @@ namespace LANCommander.PlaynitePlugin
PlayniteGame.InstallDirectory = installDirectory; PlayniteGame.InstallDirectory = installDirectory;
File.WriteAllText(Path.Combine(installDirectory, "_manifest.yml"), GetManifest(gameId)); WriteManifest(manifest, installDirectory);
SaveScript(game, installDirectory, ScriptType.Install); SaveScript(game, installDirectory, ScriptType.Install);
SaveScript(game, installDirectory, ScriptType.Uninstall); SaveScript(game, installDirectory, ScriptType.Uninstall);
@ -62,9 +64,9 @@ namespace LANCommander.PlaynitePlugin
} }
catch { } catch { }
InvokeOnInstalled(new GameInstalledEventArgs(installInfo)); Plugin.UpdateGame(manifest, gameId);
Plugin.UpdateGamesFromManifest(); InvokeOnInstalled(new GameInstalledEventArgs(installInfo));
} }
private string Download(LANCommander.SDK.Models.Game game) private string Download(LANCommander.SDK.Models.Game game)
@ -164,17 +166,15 @@ namespace LANCommander.PlaynitePlugin
return destination; 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() var serializer = new SerializerBuilder()
.WithNamingConvention(PascalCaseNamingConvention.Instance) .WithNamingConvention(PascalCaseNamingConvention.Instance)
.Build(); .Build();
var yaml = serializer.Serialize(manifest); 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) 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 Playnite.SDK.Plugins;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net.NetworkInformation; using System.Net.NetworkInformation;
@ -94,7 +95,7 @@ namespace LANCommander.PlaynitePlugin
ReleaseDate = new ReleaseDate(manifest.ReleasedOn), ReleaseDate = new ReleaseDate(manifest.ReleasedOn),
//Version = game.Archives.OrderByDescending(a => a.CreatedOn).FirstOrDefault().Version, //Version = game.Archives.OrderByDescending(a => a.CreatedOn).FirstOrDefault().Version,
Icon = new MetadataFile(iconUri.ToString()), 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, Name = a.Name,
Arguments = a.Arguments, Arguments = a.Arguments,
@ -139,7 +140,25 @@ namespace LANCommander.PlaynitePlugin
metadata.Features.Add(new MetadataNameProperty($"Online Multiplayer {manifest.OnlineMultiplayer.GetPlayerCount()}".Trim())); metadata.Features.Add(new MetadataNameProperty($"Online Multiplayer {manifest.OnlineMultiplayer.GetPlayerCount()}".Trim()));
gameMetadata.Add(metadata); 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) catch (Exception ex)
{ {
@ -272,113 +291,83 @@ namespace LANCommander.PlaynitePlugin
return window; 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)) bool isFirstAction = !manifest.Actions.Any(a => a.IsPrimaryAction) && manifest.Actions.First().Name == action.Name;
continue;
var manifestPath = Path.Combine(game.InstallDirectory, "_manifest.yml"); game.GameActions.Add(new PN.SDK.Models.GameAction()
if (File.Exists(manifestPath))
{ {
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); Name = featureName
var deserializer = new DeserializerBuilder() });
.IgnoreUnmatchedProperties()
.WithNamingConvention(PascalCaseNamingConvention.Instance)
.Build();
var manifest = deserializer.Deserialize<GameManifest>(manifestContents); game.Features.Add(new GameFeature()
#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)
{ {
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 Path { get; set; }
public string WorkingDirectory { get; set; } public string WorkingDirectory { get; set; }
public bool PrimaryAction { 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 Path { get; set; }
public string WorkingDirectory { get; set; } public string WorkingDirectory { get; set; }
public bool IsPrimaryAction { get; set; } public bool IsPrimaryAction { get; set; }
public int SortOrder { get; set; }
} }
public class MultiplayerInfo public class MultiplayerInfo

View File

@ -1,4 +1,5 @@
@using LANCommander.Data.Models @using LANCommander.Data.Models
@using LANCommander.Extensions
@{ @{
int i = 0; 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> <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; var index = i;
@ -37,8 +38,23 @@
<td> <td>
<input name="Game.Actions[@i].Id" type="hidden" value="@action.Id" /> <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].GameId" type="hidden" value="@GameId" />
<input name="Game.Actions[@i].SortOrder" type="hidden" value="@i" />
<div class="btn-list flex-nowrap justify-content-end"> <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"> <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"> <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> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
@ -55,6 +71,7 @@
<tr> <tr>
<td colspan="5"> <td colspan="5">
<div class="btn-list flex-nowrap justify-content-end"> <div class="btn-list flex-nowrap justify-content-end">
<button class="btn btn-ghost-primary" @onclick="AddAction" type="button">Add Action</button> <button class="btn btn-ghost-primary" @onclick="AddAction" type="button">Add Action</button>
</div> </div>
</td> </td>
@ -64,9 +81,18 @@
</div> </div>
@code { @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; } [Parameter] public Guid GameId { get; set; }
protected override void OnInitialized()
{
Actions = Actions.OrderBy(a => a.SortOrder).ToList();
FixSortOrders();
base.OnInitialized();
}
private void AddAction() private void AddAction()
{ {
if (Actions == null) if (Actions == null)
@ -74,12 +100,40 @@
Actions.Add(new Data.Models.Action() Actions.Add(new Data.Models.Action()
{ {
PrimaryAction = Actions.Count == 0 PrimaryAction = Actions.Count == 0,
SortOrder = Actions.Count
}); });
} }
private void RemoveAction(int index) private void RemoveAction(int index)
{ {
Actions.Remove(Actions.ElementAt(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? Path { get; set; }
public string? WorkingDirectory { get; set; } public string? WorkingDirectory { get; set; }
public bool PrimaryAction { get; set; } public bool PrimaryAction { get; set; }
public int SortOrder { get; set; }
public Guid GameId { get; set; } public Guid GameId { get; set; }
[JsonIgnore] [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") b.Property<bool>("PrimaryAction")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<int>("SortOrder")
.HasColumnType("INTEGER");
b.Property<Guid?>("UpdatedById") b.Property<Guid?>("UpdatedById")
.HasColumnType("TEXT"); .HasColumnType("TEXT");

View File

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

View File

@ -93,7 +93,7 @@
<h3 class="card-title">Actions</h3> <h3 class="card-title">Actions</h3>
</div> </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"> <div class="card-header">
<h3 class="card-title">Multiplayer Modes</h3> <h3 class="card-title">Multiplayer Modes</h3>

View File

@ -96,7 +96,7 @@
<h3 class="card-title">Actions</h3> <h3 class="card-title">Actions</h3>
</div> </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"> <div class="card-header">
<h3 class="card-title">Multiplayer Modes</h3> <h3 class="card-title">Multiplayer Modes</h3>