Rework
This commit is contained in:
parent
e86a49d5f6
commit
90d7281050
@ -1,25 +0,0 @@
|
|||||||
**/.dockerignore
|
|
||||||
**/.env
|
|
||||||
**/.git
|
|
||||||
**/.gitignore
|
|
||||||
**/.project
|
|
||||||
**/.settings
|
|
||||||
**/.toolstarget
|
|
||||||
**/.vs
|
|
||||||
**/.vscode
|
|
||||||
**/.idea
|
|
||||||
**/*.*proj.user
|
|
||||||
**/*.dbmdl
|
|
||||||
**/*.jfm
|
|
||||||
**/azds.yaml
|
|
||||||
**/bin
|
|
||||||
**/charts
|
|
||||||
**/docker-compose*
|
|
||||||
**/Dockerfile*
|
|
||||||
**/node_modules
|
|
||||||
**/npm-debug.log
|
|
||||||
**/obj
|
|
||||||
**/secrets.dev.yaml
|
|
||||||
**/values.dev.yaml
|
|
||||||
LICENSE
|
|
||||||
README.md
|
|
22
API/API.csproj
Normal file
22
API/API.csproj
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.4"/>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Controllers\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\SQLiteEF\SQLiteEF.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
33
SQLiteEF/Context.cs
Normal file
33
SQLiteEF/Context.cs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
|
namespace SQLiteEF;
|
||||||
|
|
||||||
|
public class Context(IConfiguration configuration) : DbContext
|
||||||
|
{
|
||||||
|
public DbSet<Player> Players { get; set; }
|
||||||
|
public DbSet<Game> Games { get; set; }
|
||||||
|
|
||||||
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||||
|
{
|
||||||
|
optionsBuilder.UseSqlite(configuration.GetConnectionString("DefaultConnection"));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
modelBuilder.Entity<Player>()
|
||||||
|
.HasMany<Game>(p => p.Games)
|
||||||
|
.WithMany(g => g.PlayedBy);
|
||||||
|
modelBuilder.Entity<Player>()
|
||||||
|
.Navigation(p => p.Games)
|
||||||
|
.AutoInclude();
|
||||||
|
modelBuilder.Entity<TrackedTime>()
|
||||||
|
.HasOne<Player>(p => p.Player)
|
||||||
|
.WithMany(p => p.TrackedTimes)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
modelBuilder.Entity<TrackedTime>()
|
||||||
|
.HasOne<Game>(p => p.Game)
|
||||||
|
.WithMany(g => g.TrackedTimes)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
}
|
||||||
|
}
|
12
SQLiteEF/Game.cs
Normal file
12
SQLiteEF/Game.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace SQLiteEF;
|
||||||
|
|
||||||
|
[PrimaryKey("AppId")]
|
||||||
|
public class Game(ulong appId, string name)
|
||||||
|
{
|
||||||
|
public ulong AppId { get; init; } = appId;
|
||||||
|
public string Name { get; init; } = name;
|
||||||
|
public ICollection<Player> PlayedBy { get; init; } = null!;
|
||||||
|
public ICollection<TrackedTime> TrackedTimes { get; init; } = null!;
|
||||||
|
}
|
6
SQLiteEF/IUpdateable.cs
Normal file
6
SQLiteEF/IUpdateable.cs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
namespace SQLiteEF;
|
||||||
|
|
||||||
|
public interface IUpdateable
|
||||||
|
{
|
||||||
|
public DateTime UpdatedAt { get; set; }
|
||||||
|
}
|
29
SQLiteEF/TrackedTime.cs
Normal file
29
SQLiteEF/TrackedTime.cs
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace SQLiteEF;
|
||||||
|
|
||||||
|
[PrimaryKey("TimeStamp")]
|
||||||
|
public class TrackedTime
|
||||||
|
{
|
||||||
|
public Game Game { get; init; }
|
||||||
|
public Player Player { get; init; }
|
||||||
|
public DateTime TimeStamp { get; init; }
|
||||||
|
public ulong TimePlayed { get; init; }
|
||||||
|
|
||||||
|
public TrackedTime(Game game, Player player, ulong timePlayed, DateTime? timeStamp = null)
|
||||||
|
{
|
||||||
|
this.Game = game;
|
||||||
|
this.Player = player;
|
||||||
|
this.TimeStamp = timeStamp??DateTime.Now;
|
||||||
|
this.TimePlayed = timePlayed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// EF CORE
|
||||||
|
/// </summary>
|
||||||
|
internal TrackedTime(ulong timePlayed, DateTime timeStamp)
|
||||||
|
{
|
||||||
|
this.TimePlayed = timePlayed;
|
||||||
|
this.TimeStamp = timeStamp;
|
||||||
|
}
|
||||||
|
}
|
3
SteamApiWrapper/ReturnTypes/Game.cs
Normal file
3
SteamApiWrapper/ReturnTypes/Game.cs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
namespace SteamApiWrapper.ReturnTypes;
|
||||||
|
|
||||||
|
public record Game(ulong appid, ulong playtime_forever, string name);
|
3
SteamApiWrapper/ReturnTypes/GetOwnedGames.cs
Normal file
3
SteamApiWrapper/ReturnTypes/GetOwnedGames.cs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
namespace SteamApiWrapper.ReturnTypes;
|
||||||
|
|
||||||
|
public record GetOwnedGames(uint game_count, Game[] games) : IReturnType;
|
3
SteamApiWrapper/ReturnTypes/GetPlayerSummaries.cs
Normal file
3
SteamApiWrapper/ReturnTypes/GetPlayerSummaries.cs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
namespace SteamApiWrapper.ReturnTypes;
|
||||||
|
|
||||||
|
public record GetPlayerSummaries(Player[] players) : IReturnType;
|
3
SteamApiWrapper/ReturnTypes/GetRecentlyPlayedGames.cs
Normal file
3
SteamApiWrapper/ReturnTypes/GetRecentlyPlayedGames.cs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
namespace SteamApiWrapper.ReturnTypes;
|
||||||
|
|
||||||
|
public record GetRecentlyPlayedGames(uint total_count, Game[] games) : IReturnType;
|
3
SteamApiWrapper/ReturnTypes/GetSupportedAPIList.cs
Normal file
3
SteamApiWrapper/ReturnTypes/GetSupportedAPIList.cs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
namespace SteamApiWrapper.ReturnTypes;
|
||||||
|
|
||||||
|
public record GetSupportedAPIList(SupportedAPIInterface[] Interfaces) : IReturnType;
|
6
SteamApiWrapper/ReturnTypes/IReturnType.cs
Normal file
6
SteamApiWrapper/ReturnTypes/IReturnType.cs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
namespace SteamApiWrapper.ReturnTypes;
|
||||||
|
|
||||||
|
public interface IReturnType
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
27
SteamApiWrapper/ReturnTypes/Player.cs
Normal file
27
SteamApiWrapper/ReturnTypes/Player.cs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
namespace SteamApiWrapper.ReturnTypes;
|
||||||
|
|
||||||
|
public record Player(ulong steamid,
|
||||||
|
CommunityVisibilityState communityvisibilitystate,
|
||||||
|
bool profilestate,
|
||||||
|
string personaname,
|
||||||
|
string profileurl,
|
||||||
|
string avatar,
|
||||||
|
PersonaState personastate,
|
||||||
|
string? realname);
|
||||||
|
|
||||||
|
public enum PersonaState : byte
|
||||||
|
{
|
||||||
|
Offline = 0,
|
||||||
|
Online = 1,
|
||||||
|
Busy = 2,
|
||||||
|
Away = 3,
|
||||||
|
Snooze = 4,
|
||||||
|
LookingToTrade = 5,
|
||||||
|
LookingToPlay = 6
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum CommunityVisibilityState : byte
|
||||||
|
{
|
||||||
|
Hidden = 1,
|
||||||
|
Public = 3
|
||||||
|
}
|
13
SteamApiWrapper/ReturnTypes/SupportedAPIInterface.cs
Normal file
13
SteamApiWrapper/ReturnTypes/SupportedAPIInterface.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
namespace SteamApiWrapper.ReturnTypes;
|
||||||
|
|
||||||
|
public struct SupportedAPIInterface(string name, SupportedAPIMethod[]? methods = null)
|
||||||
|
{
|
||||||
|
public readonly string Name = name;
|
||||||
|
public readonly SupportedAPIMethod[]? Methods = methods;
|
||||||
|
|
||||||
|
public override string ToString() => Name;
|
||||||
|
|
||||||
|
public static SupportedAPIInterface IPlayerService = new("IPlayerService");
|
||||||
|
public static SupportedAPIInterface ISteamWebAPIUtil = new("ISteamWebAPIUtil");
|
||||||
|
public static SupportedAPIInterface ISteamUser = new("ISteamUser");
|
||||||
|
}
|
7
SteamApiWrapper/ReturnTypes/SupportedAPIMethod.cs
Normal file
7
SteamApiWrapper/ReturnTypes/SupportedAPIMethod.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
namespace SteamApiWrapper.ReturnTypes;
|
||||||
|
|
||||||
|
public struct SupportedAPIMethod(string name, int version)
|
||||||
|
{
|
||||||
|
public string Name = name;
|
||||||
|
public int Version = version;
|
||||||
|
}
|
106
SteamApiWrapper/SteamApiWrapper.cs
Normal file
106
SteamApiWrapper/SteamApiWrapper.cs
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using SteamApiWrapper.ReturnTypes;
|
||||||
|
|
||||||
|
namespace SteamApiWrapper;
|
||||||
|
|
||||||
|
public static class SteamApiWrapper
|
||||||
|
{
|
||||||
|
public const string ApiUrl = "http://api.steampowered.com/";
|
||||||
|
private static string _apiKey = string.Empty;
|
||||||
|
private static HttpClient client = new ();
|
||||||
|
|
||||||
|
public static void SetApiKey(string apiKey)
|
||||||
|
{
|
||||||
|
_apiKey = apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HttpRequestMessage BuildRequest(SupportedAPIInterface apiInterface, string methodName, string version = "v0001", Dictionary<string, string>? opts = null)
|
||||||
|
{
|
||||||
|
string url = $"{ApiUrl}/{apiInterface}/{methodName}/{version}?key={_apiKey}&format=json";
|
||||||
|
if (opts != null)
|
||||||
|
foreach ((string key, string value) in opts)
|
||||||
|
url += $"&{key}={value}";
|
||||||
|
|
||||||
|
HttpRequestMessage ret = new(HttpMethod.Get, url);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static GetOwnedGames GetOwnedGames(ulong steamid)
|
||||||
|
{
|
||||||
|
HttpRequestMessage request = BuildRequest(SupportedAPIInterface.IPlayerService, "GetOwnedGames", opts: new()
|
||||||
|
{
|
||||||
|
{"steamid", steamid.ToString()},
|
||||||
|
{"include_appinfo", "true"},
|
||||||
|
{"include_played_free_games", "true"}
|
||||||
|
});
|
||||||
|
try
|
||||||
|
{
|
||||||
|
HttpResponseMessage response = client.Send(request);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
JObject jObj = JObject.Parse(response.Content.ReadAsStringAsync().Result);
|
||||||
|
return jObj["response"]?.ToObject<GetOwnedGames>()??new(0, []);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Console.WriteLine(e.Message);
|
||||||
|
return new(0, []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static GetPlayerSummaries GetPlayerSummaries(ulong[] steamids)
|
||||||
|
{
|
||||||
|
HttpRequestMessage request = BuildRequest(SupportedAPIInterface.ISteamUser, "GetPlayerSummaries", "v0002", opts: new()
|
||||||
|
{
|
||||||
|
{"steamids", string.Join(',', steamids)}
|
||||||
|
});
|
||||||
|
try
|
||||||
|
{
|
||||||
|
HttpResponseMessage response = client.Send(request);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
JObject jObj = JObject.Parse(response.Content.ReadAsStringAsync().Result);
|
||||||
|
return jObj["response"]?["players"]?.ToObject<GetPlayerSummaries>()??new([]);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Console.WriteLine(e.Message);
|
||||||
|
return new([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static GetRecentlyPlayedGames GetRecentlyPlayedGames(ulong steamid)
|
||||||
|
{
|
||||||
|
HttpRequestMessage request = BuildRequest(SupportedAPIInterface.IPlayerService, "GetRecentlyPlayedGames", opts: new()
|
||||||
|
{
|
||||||
|
{"steamid", steamid.ToString()}
|
||||||
|
});
|
||||||
|
try
|
||||||
|
{
|
||||||
|
HttpResponseMessage response = client.Send(request);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
JObject jObj = JObject.Parse(response.Content.ReadAsStringAsync().Result);
|
||||||
|
return jObj["response"]?.ToObject<GetRecentlyPlayedGames>()??new(0, []);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Console.WriteLine(e.Message);
|
||||||
|
return new(0, []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static GetSupportedAPIList GetSupportedAPIList()
|
||||||
|
{
|
||||||
|
HttpRequestMessage request = BuildRequest(SupportedAPIInterface.ISteamWebAPIUtil, "GetSupportedAPIList");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
HttpResponseMessage response = client.Send(request);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
JObject jObj = JObject.Parse(response.Content.ReadAsStringAsync().Result);
|
||||||
|
return jObj["apilist"]?.ToObject<GetSupportedAPIList>()??new([]);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Console.WriteLine(e.Message);
|
||||||
|
return new([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
14
SteamApiWrapper/SteamApiWrapper.csproj
Normal file
14
SteamApiWrapper/SteamApiWrapper.csproj
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
@ -1,6 +1,14 @@
|
|||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SteamGameTimeTrack", "SteamGameTimeTrack\SteamGameTimeTrack.csproj", "{40948C87-098B-48A5-BABA-02395BE797C2}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "API", "API\API.csproj", "{92752A41-5C89-47E0-9588-379AC9EFD706}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SteamApiWrapper", "SteamApiWrapper\SteamApiWrapper.csproj", "{53B416A7-E3B9-4F19-9A71-16CA115BB127}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Run", "Run\Run.csproj", "{959773AD-4671-4FC7-9729-0EE4A971918E}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tracker", "Tracker\Tracker.csproj", "{59876ADC-5DF9-4672-8AA2-F6943966ABF5}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SQLiteEF", "SQLiteEF\SQLiteEF.csproj", "{A269EE55-A8E1-4421-9A70-D6F36D162C4D}"
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
@ -8,9 +16,25 @@ Global
|
|||||||
Release|Any CPU = Release|Any CPU
|
Release|Any CPU = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
{40948C87-098B-48A5-BABA-02395BE797C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{92752A41-5C89-47E0-9588-379AC9EFD706}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{40948C87-098B-48A5-BABA-02395BE797C2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{92752A41-5C89-47E0-9588-379AC9EFD706}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{40948C87-098B-48A5-BABA-02395BE797C2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{92752A41-5C89-47E0-9588-379AC9EFD706}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{40948C87-098B-48A5-BABA-02395BE797C2}.Release|Any CPU.Build.0 = Release|Any CPU
|
{92752A41-5C89-47E0-9588-379AC9EFD706}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{53B416A7-E3B9-4F19-9A71-16CA115BB127}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{53B416A7-E3B9-4F19-9A71-16CA115BB127}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{53B416A7-E3B9-4F19-9A71-16CA115BB127}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{53B416A7-E3B9-4F19-9A71-16CA115BB127}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{959773AD-4671-4FC7-9729-0EE4A971918E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{959773AD-4671-4FC7-9729-0EE4A971918E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{959773AD-4671-4FC7-9729-0EE4A971918E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{959773AD-4671-4FC7-9729-0EE4A971918E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{59876ADC-5DF9-4672-8AA2-F6943966ABF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{59876ADC-5DF9-4672-8AA2-F6943966ABF5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{59876ADC-5DF9-4672-8AA2-F6943966ABF5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{59876ADC-5DF9-4672-8AA2-F6943966ABF5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{A269EE55-A8E1-4421-9A70-D6F36D162C4D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{A269EE55-A8E1-4421-9A70-D6F36D162C4D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{A269EE55-A8E1-4421-9A70-D6F36D162C4D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{A269EE55-A8E1-4421-9A70-D6F36D162C4D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=appinfo/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=appinfo/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=friendslist/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=friendslist/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=steamid/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=steamid/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=steamids/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
@ -1,260 +0,0 @@
|
|||||||
using System.Net;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
|
|
||||||
namespace SteamGameTimeTrack;
|
|
||||||
|
|
||||||
public class API : IDisposable
|
|
||||||
{
|
|
||||||
private readonly HttpListener _server = new();
|
|
||||||
private Thread listenThread;
|
|
||||||
private CancellationTokenSource cts = new();
|
|
||||||
private ILogger logger;
|
|
||||||
private readonly Tracker parent;
|
|
||||||
|
|
||||||
public API(Tracker parent, int port, ILogger logger)
|
|
||||||
{
|
|
||||||
this.parent = parent;
|
|
||||||
this.logger = logger;
|
|
||||||
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
|
||||||
this._server.Prefixes.Add($"http://*:{port}/");
|
|
||||||
else
|
|
||||||
this._server.Prefixes.Add($"http://localhost:{port}/");
|
|
||||||
this.listenThread = new (Listen);
|
|
||||||
this.listenThread.Start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Listen()
|
|
||||||
{
|
|
||||||
this._server.Start();
|
|
||||||
foreach (string serverPrefix in this._server.Prefixes)
|
|
||||||
logger.LogInformation($"Listening on {serverPrefix}");
|
|
||||||
while (this._server.IsListening)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
//cts.CancelAfter(1000);
|
|
||||||
HttpListenerContext context = this._server.GetContext();
|
|
||||||
//Log($"{context.Request.HttpMethod} {context.Request.Url} {context.Request.UserAgent}");
|
|
||||||
Task t = new(() =>
|
|
||||||
{
|
|
||||||
HandleRequest(context);
|
|
||||||
});
|
|
||||||
t.Start();
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
this.logger.LogError(e, e.ToString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleRequest(HttpListenerContext context)
|
|
||||||
{
|
|
||||||
HttpListenerRequest request = context.Request;
|
|
||||||
HttpListenerResponse response = context.Response;
|
|
||||||
if (request.Url!.LocalPath.Contains("favicon"))
|
|
||||||
{
|
|
||||||
SendResponse(HttpStatusCode.NoContent, response);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (request.HttpMethod)
|
|
||||||
{
|
|
||||||
case "GET":
|
|
||||||
HandleGet(request, response);
|
|
||||||
break;
|
|
||||||
case "OPTIONS":
|
|
||||||
SendResponse(HttpStatusCode.OK, response);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
SendResponse(HttpStatusCode.MethodNotAllowed, response);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Dictionary<string, string> GetRequestVariables(string query)
|
|
||||||
{
|
|
||||||
Dictionary<string, string> ret = new();
|
|
||||||
Regex queryRex = new (@"\?{1}&?([A-z0-9-=]+=[A-z0-9-=]+)+(&[A-z0-9-=]+=[A-z0-9-=]+)*");
|
|
||||||
if (!queryRex.IsMatch(query))
|
|
||||||
return ret;
|
|
||||||
query = query.Substring(1);
|
|
||||||
foreach (string keyValuePair in query.Split('&').Where(str => str.Length >= 3))
|
|
||||||
{
|
|
||||||
string var = keyValuePair.Split('=')[0];
|
|
||||||
string val = Regex.Replace(keyValuePair.Substring(var.Length + 1), "%20", " ");
|
|
||||||
val = Regex.Replace(val, "%[0-9]{2}", "");
|
|
||||||
ret.Add(var, val);
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void HandleGet(HttpListenerRequest request, HttpListenerResponse response)
|
|
||||||
{
|
|
||||||
Dictionary<string, string> requestVariables = GetRequestVariables(request.Url!.Query);
|
|
||||||
string path = Regex.Match(request.Url!.LocalPath, @"[A-z0-9]+(\/[A-z0-9]+)*").Value;
|
|
||||||
Regex playerInfoRex = new(@"Player(?:\/([0-9]+))?");
|
|
||||||
Regex sessionInfoRex = new(@"Player\/([0-9]+|[A-z0-9]+)\/Session(?:\/([0-9]+))?");
|
|
||||||
Regex gameTimeInfoRex = new(@"Player\/([0-9]+|[A-z0-9]+)\/GameTime(?:\/([0-9]+))?");
|
|
||||||
Regex playerGameInfo = new(@"Player\/([0-9]+|[A-z0-9]+)(?:\/([0-9]+))");
|
|
||||||
|
|
||||||
this.logger.LogInformation($"API GET {path}");
|
|
||||||
|
|
||||||
if (gameTimeInfoRex.Match(path) is { Success: true } gti && gti.Value.Equals(path))
|
|
||||||
{
|
|
||||||
if (parent._players.Any(p => p.steamid.Equals(gti.Groups[1].Value)))
|
|
||||||
{
|
|
||||||
Player player = parent._players.First(p => p.steamid.Equals(gti.Groups[1].Value));
|
|
||||||
if (gti.Groups[2].Success && int.TryParse(gti.Groups[2].Value, out int appid)) //Return specific game
|
|
||||||
{
|
|
||||||
if (player.GameTimes.Any(g => g.appid == appid))
|
|
||||||
SendResponse(HttpStatusCode.OK, response, player.GameTimes.First(g => g.appid == appid));
|
|
||||||
else
|
|
||||||
SendResponse(HttpStatusCode.NotFound, response, $"AppId {appid} not found");
|
|
||||||
}
|
|
||||||
else //Return all games
|
|
||||||
{
|
|
||||||
SendResponse(HttpStatusCode.OK, response, player.GameTimes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
SendResponse(HttpStatusCode.NotFound, response, $"Player {gti.Groups[1].Value} not found");
|
|
||||||
}else if (sessionInfoRex.Match(path) is { Success: true } si&& si.Value.Equals(path))
|
|
||||||
{
|
|
||||||
if (parent._players.Any(p => p.steamid.Equals(si.Groups[1].Value)))
|
|
||||||
{
|
|
||||||
Player player = parent._players.First(p => p.steamid.Equals(si.Groups[1].Value));
|
|
||||||
if (si.Groups[2].Success && int.TryParse(si.Groups[2].Value, out int appid)) //Return specific game
|
|
||||||
{
|
|
||||||
if (player.Sessions.Any(s => s.AppId == appid))
|
|
||||||
SendResponse(HttpStatusCode.OK, response, player.Sessions.Where(s => s.AppId == appid));
|
|
||||||
else
|
|
||||||
SendResponse(HttpStatusCode.NotFound, response, $"No Sessions for AppId {appid} found");
|
|
||||||
}
|
|
||||||
else //Return all games
|
|
||||||
{
|
|
||||||
SendResponse(HttpStatusCode.OK, response, player.Sessions);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
SendResponse(HttpStatusCode.NotFound, response, $"Player {si.Groups[1].Value} not found");
|
|
||||||
}else if(playerGameInfo.Match(path) is { Success: true } gi&& gi.Value.Equals(path))
|
|
||||||
{
|
|
||||||
if (parent._players.Any(p => p.steamid.Equals(gi.Groups[1].Value)))
|
|
||||||
{
|
|
||||||
Player player = parent._players.First(p => p.steamid.Equals(gi.Groups[1].Value));
|
|
||||||
if (gi.Groups[2].Success && int.TryParse(gi.Groups[2].Value, out int appid)) //Return specific game
|
|
||||||
{
|
|
||||||
JObject x = new();
|
|
||||||
if (player.Sessions.Any(s => s.AppId == appid))
|
|
||||||
x["Sessions"] = new JArray(player.Sessions.Where(s => s.AppId == appid));
|
|
||||||
else
|
|
||||||
x["Sessions"] = null;
|
|
||||||
if (player.GameTimes.Any(g => g.appid == appid))
|
|
||||||
x["GameTime"] = JObject.FromObject(player.GameTimes.First(g => g.appid == appid));
|
|
||||||
else
|
|
||||||
x["GameTime"] = null;
|
|
||||||
|
|
||||||
if(x["Sessions"] != null && x["GameTime"] != null)
|
|
||||||
SendResponse(HttpStatusCode.OK, response, x);
|
|
||||||
else
|
|
||||||
SendResponse(HttpStatusCode.NotFound, response, $"AppId {appid} not found");
|
|
||||||
}
|
|
||||||
else //Return all games
|
|
||||||
{
|
|
||||||
SendResponse(HttpStatusCode.BadRequest, response);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
SendResponse(HttpStatusCode.NotFound, response, $"Player {gi.Groups[1].Value} not found");
|
|
||||||
}else if (playerInfoRex.Match(path) is { Success: true } pi&& pi.Value.Equals(path))
|
|
||||||
{
|
|
||||||
if (pi.Groups[1].Success)//Return specific Player
|
|
||||||
{
|
|
||||||
if(parent._players.Any(p => p.steamid.Equals(pi.Groups[1].Value)))
|
|
||||||
SendResponse(HttpStatusCode.OK, response, parent._players.First(p => p.steamid.Equals(pi.Groups[1].Value)));
|
|
||||||
else
|
|
||||||
SendResponse(HttpStatusCode.NotFound, response, $"Player {pi.Groups[1].Value} not found");
|
|
||||||
}
|
|
||||||
else//Return all Players
|
|
||||||
SendResponse(HttpStatusCode.OK, response, parent._players);
|
|
||||||
}else
|
|
||||||
SendResponse(HttpStatusCode.NotFound, response, "Path not found.\n" +
|
|
||||||
"Valid Paths:\n" +
|
|
||||||
"\t/Player/<SteamID64>\n" +
|
|
||||||
"\t/Player/<SteamID64>/<appid>\n" +
|
|
||||||
"\t/Player/<SteamID64>/GameTime/<appid>\n" +
|
|
||||||
"\t/Player/<SteamID64>/Session/<appid>\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SendResponse(HttpStatusCode statusCode, HttpListenerResponse response, object? content = null)
|
|
||||||
{
|
|
||||||
logger.LogInformation($"Response: {statusCode.ToString()}");
|
|
||||||
response.StatusCode = (int)statusCode;
|
|
||||||
response.AddHeader("Access-Control-Allow-Headers", "Content-Type, Accept, X-Requested-With");
|
|
||||||
response.AddHeader("Access-Control-Allow-Methods", "GET");
|
|
||||||
response.AddHeader("Access-Control-Max-Age", "5");
|
|
||||||
response.AppendHeader("Access-Control-Allow-Origin", "*");
|
|
||||||
|
|
||||||
byte[] bytes;
|
|
||||||
if (content is not null)
|
|
||||||
{
|
|
||||||
if (content is string str)
|
|
||||||
bytes = Encoding.UTF8.GetBytes(str);
|
|
||||||
else if (content is JObject jo)
|
|
||||||
bytes = Encoding.UTF8.GetBytes(jo.ToString());
|
|
||||||
else
|
|
||||||
bytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(content));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
bytes = Array.Empty<byte>();
|
|
||||||
|
|
||||||
if (content is not Stream)
|
|
||||||
{
|
|
||||||
response.ContentType = content?.GetType() == typeof(string) ? "text/plain" : "application/json";
|
|
||||||
try
|
|
||||||
{
|
|
||||||
response.OutputStream.Write(bytes);
|
|
||||||
response.OutputStream.Close();
|
|
||||||
}
|
|
||||||
catch (HttpListenerException e)
|
|
||||||
{
|
|
||||||
logger.LogError(e, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if(content is FileStream stream)
|
|
||||||
{
|
|
||||||
string contentType = stream.Name.Split('.')[^1];
|
|
||||||
switch (contentType.ToLower())
|
|
||||||
{
|
|
||||||
case "gif":
|
|
||||||
response.ContentType = "image/gif";
|
|
||||||
break;
|
|
||||||
case "png":
|
|
||||||
response.ContentType = "image/png";
|
|
||||||
break;
|
|
||||||
case "jpg":
|
|
||||||
case "jpeg":
|
|
||||||
response.ContentType = "image/jpeg";
|
|
||||||
break;
|
|
||||||
case "log":
|
|
||||||
response.ContentType = "text/plain";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
stream.CopyTo(response.OutputStream);
|
|
||||||
response.OutputStream.Close();
|
|
||||||
stream.Close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
((IDisposable)_server).Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace SteamGameTimeTrack;
|
|
||||||
|
|
||||||
public struct Config
|
|
||||||
{
|
|
||||||
public string SteamId, ApiToken;
|
|
||||||
public bool TrackFriends;
|
|
||||||
public LogLevel LogLevel;
|
|
||||||
public int ApiPort;
|
|
||||||
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return $"SteamId: {SteamId}\nTrack Friends: {(TrackFriends ? "yes" : "no")}\nApi-Port: {ApiPort}\nLogLevel: {Enum.GetName(LogLevel)}\nApiToken: ****{ApiToken.Substring(4, ApiToken.Length-4)}****";
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
FROM mcr.microsoft.com/dotnet/runtime:7.0 AS base
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
|
|
||||||
ARG BUILD_CONFIGURATION=Release
|
|
||||||
WORKDIR /src
|
|
||||||
COPY ["SteamGameTimeTrack/SteamGameTimeTrack.csproj", "SteamGameTimeTrack/"]
|
|
||||||
RUN dotnet restore "SteamGameTimeTrack/SteamGameTimeTrack.csproj"
|
|
||||||
COPY . .
|
|
||||||
WORKDIR "/src/SteamGameTimeTrack"
|
|
||||||
RUN dotnet build "SteamGameTimeTrack.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
|
||||||
|
|
||||||
FROM build AS publish
|
|
||||||
ARG BUILD_CONFIGURATION=Release
|
|
||||||
RUN dotnet publish "SteamGameTimeTrack.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
|
||||||
|
|
||||||
FROM base AS final
|
|
||||||
WORKDIR /app
|
|
||||||
COPY --from=publish /app/publish .
|
|
||||||
ENTRYPOINT ["dotnet", "SteamGameTimeTrack.dll"]
|
|
@ -1,28 +0,0 @@
|
|||||||
// ReSharper disable InconsistentNaming
|
|
||||||
// ReSharper disable MemberCanBePrivate.Global
|
|
||||||
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace SteamGameTimeTrack;
|
|
||||||
|
|
||||||
public struct GameTime
|
|
||||||
{
|
|
||||||
public int appid, playtime_forever;
|
|
||||||
public DateTime? GameStarted;
|
|
||||||
public string name;
|
|
||||||
[JsonIgnore]
|
|
||||||
public TimeSpan PlayTime => TimeSpan.FromMinutes(playtime_forever);
|
|
||||||
[JsonIgnore]
|
|
||||||
public TimeSpan? Session => GameStarted is not null ? DateTime.UtcNow.Subtract(GameStarted.Value) : null;
|
|
||||||
|
|
||||||
public GameTime Clone()
|
|
||||||
{
|
|
||||||
return new GameTime()
|
|
||||||
{
|
|
||||||
appid = this.appid,
|
|
||||||
GameStarted = this.GameStarted,
|
|
||||||
name = this.name,
|
|
||||||
playtime_forever = this.playtime_forever
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,63 +0,0 @@
|
|||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
|
|
||||||
namespace SteamGameTimeTrack;
|
|
||||||
|
|
||||||
public class Net
|
|
||||||
{
|
|
||||||
private readonly ILogger _logger;
|
|
||||||
private readonly string _apiToken;
|
|
||||||
private DateTime lastRequest = DateTime.UtcNow;
|
|
||||||
private readonly TimeSpan _minTimeBetweenRequests = TimeSpan.FromMilliseconds(10);
|
|
||||||
|
|
||||||
public Net(ILogger logger, string apiToken)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_apiToken = apiToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal HttpResponseMessage MakeRequest(string uri, string? requestId = null)
|
|
||||||
{
|
|
||||||
if (requestId is null)
|
|
||||||
{
|
|
||||||
requestId = RandomString(4);
|
|
||||||
this._logger.LogDebug($"RequestId: {requestId} for URI {uri}");
|
|
||||||
}
|
|
||||||
HttpClient client = new();
|
|
||||||
uri = uri.Contains("format=json") ? uri : $"{uri}&format=json";
|
|
||||||
uri = uri.Contains("key=") ? uri : $"{uri}&key={_apiToken}";
|
|
||||||
HttpRequestMessage request = new (HttpMethod.Get, uri);
|
|
||||||
TimeSpan wait = this.lastRequest.Add(_minTimeBetweenRequests).Subtract(DateTime.UtcNow);
|
|
||||||
if (wait > TimeSpan.Zero)
|
|
||||||
{
|
|
||||||
this._logger.LogDebug($"Waiting {wait:s\\.fffff's'} before requesting {requestId}...");
|
|
||||||
Thread.Sleep(wait);
|
|
||||||
}else
|
|
||||||
this._logger.LogDebug($"Requesting {requestId}...");
|
|
||||||
HttpResponseMessage response = client.Send(request, HttpCompletionOption.ResponseContentRead);
|
|
||||||
this.lastRequest = DateTime.UtcNow;
|
|
||||||
this._logger.LogDebug($"Request {requestId} -> {response.StatusCode}");
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal JObject? MakeRequestGetJObject(string uri, string? requestId = null)
|
|
||||||
{
|
|
||||||
if (requestId is null)
|
|
||||||
{
|
|
||||||
requestId = RandomString(4);
|
|
||||||
this._logger.LogDebug($"RequestId: {requestId} for URI {uri}");
|
|
||||||
}
|
|
||||||
HttpResponseMessage response = MakeRequest(uri, requestId);
|
|
||||||
if (!response.IsSuccessStatusCode)
|
|
||||||
return null;
|
|
||||||
this._logger.LogDebug($"Parsing JObject for {requestId}...");
|
|
||||||
return JObject.Parse(response.Content.ReadAsStringAsync().Result);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string RandomString(int length)
|
|
||||||
{
|
|
||||||
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
|
||||||
return new string(Enumerable.Repeat(chars, length)
|
|
||||||
.Select(s => s[Random.Shared.Next(s.Length)]).ToArray());
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,228 +0,0 @@
|
|||||||
// ReSharper disable IdentifierTypo
|
|
||||||
// ReSharper disable InconsistentNaming
|
|
||||||
// ReSharper disable MemberCanBePrivate.Global
|
|
||||||
// ReSharper disable UnassignedField.Global
|
|
||||||
// ReSharper disable PropertyCanBeMadeInitOnly.Global
|
|
||||||
|
|
||||||
using System.Reflection;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
|
|
||||||
namespace SteamGameTimeTrack;
|
|
||||||
|
|
||||||
public struct Player
|
|
||||||
{
|
|
||||||
private readonly Net net;
|
|
||||||
|
|
||||||
public string steamid { get; init; }
|
|
||||||
public bool IsMe { get; init; }
|
|
||||||
|
|
||||||
public PersonaStateEnum personastate { get; set; } = PersonaStateEnum.Offline;
|
|
||||||
public byte communityvisibilitystate { get; set; } = 0;
|
|
||||||
public bool profilestate { get; set; } = false;
|
|
||||||
public long lastlogoff { get; set; } = 0;
|
|
||||||
public bool? commentpermission { get; set; } = null;
|
|
||||||
|
|
||||||
public string? realname { get; set; } = null;
|
|
||||||
public string? primaryclanid { get; set; } = null;
|
|
||||||
public string? gameserverip { get; set; } = null;
|
|
||||||
public string? gameextrainfo { get; set; } = null;
|
|
||||||
public string? loccountrycode { get; set; } = null;
|
|
||||||
public string? locstatecode { get; set; } = null;
|
|
||||||
public string? loccityid { get; set; } = null;
|
|
||||||
public string? personaname { get; set; } = null;
|
|
||||||
public string? profileurl { get; set; } = null;
|
|
||||||
public string? avatar { get; set; } = null;
|
|
||||||
public string? avatarmedium { get; set; } = null;
|
|
||||||
public string? avatarfull { get; set; } = null;
|
|
||||||
public string? gameid { get; set; } = null;
|
|
||||||
public long timecreated { get; set; } = 0;
|
|
||||||
public long friend_since { get; set; } = 0;
|
|
||||||
public HashSet<GameTime> GameTimes = new();
|
|
||||||
// ReSharper disable once CollectionNeverQueried.Global
|
|
||||||
public List<Session> Sessions = new();
|
|
||||||
|
|
||||||
[JsonIgnore]
|
|
||||||
public DateTime? TimeCreated => DateTime.UnixEpoch.AddSeconds(timecreated);
|
|
||||||
[JsonIgnore]
|
|
||||||
public DateTime? FriendSince => DateTime.UnixEpoch.AddSeconds(friend_since);
|
|
||||||
[JsonIgnore]
|
|
||||||
public DateTime? LastLogoff => DateTime.UnixEpoch.AddSeconds(lastlogoff);
|
|
||||||
|
|
||||||
[JsonConstructor]
|
|
||||||
public Player(string steamid, Net net, bool IsMe = false)
|
|
||||||
{
|
|
||||||
this.IsMe = IsMe;
|
|
||||||
this.net = net;
|
|
||||||
this.steamid = steamid;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int GetHashCode()
|
|
||||||
{
|
|
||||||
return steamid.GetHashCode();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Export()
|
|
||||||
{
|
|
||||||
string path = Path.Join("states", steamid);
|
|
||||||
if (!Directory.Exists(path))
|
|
||||||
Directory.CreateDirectory(path);
|
|
||||||
File.WriteAllText(Path.Join(path, $"{DateTime.UtcNow:dd-mm-yy hh-mm-ss}.json"), JsonConvert.SerializeObject(this, Formatting.Indented));
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Diffs(Player other, out string diffStr)
|
|
||||||
{
|
|
||||||
List<string> diffs = new();
|
|
||||||
foreach (FieldInfo field in GetType().GetFields())
|
|
||||||
{
|
|
||||||
object? thisValue = field.GetValue(this);
|
|
||||||
object? otherValue = field.GetValue(other);
|
|
||||||
if(thisValue is not null && otherValue is not null && !thisValue.Equals(otherValue))
|
|
||||||
diffs.Add($"{field.Name} {thisValue} -> {otherValue}");
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (PropertyInfo property in GetType().GetProperties())
|
|
||||||
{
|
|
||||||
object? thisValue = property.GetValue(this);
|
|
||||||
object? otherValue = property.GetValue(other);
|
|
||||||
if(thisValue is not null && otherValue is not null && !thisValue.Equals(otherValue))
|
|
||||||
diffs.Add($"{property.Name} {thisValue} -> {otherValue}");
|
|
||||||
}
|
|
||||||
|
|
||||||
GameTime[] newGameTimes = other.GameTimes.Except(GameTimes).ToArray();
|
|
||||||
foreach (GameTime newGameTime in newGameTimes)
|
|
||||||
diffs.Add($"New game {newGameTime.name} {newGameTime.appid} {newGameTime.PlayTime}");
|
|
||||||
foreach (GameTime otherGameTime in other.GameTimes)
|
|
||||||
// ReSharper disable once PatternAlwaysMatches
|
|
||||||
if(this.GameTimes.First(tgt => tgt.appid == otherGameTime.appid) is GameTime thisGameTime && !thisGameTime.GameStarted.Equals(otherGameTime.GameStarted))
|
|
||||||
diffs.Add($"Started {otherGameTime.name} {otherGameTime.appid} at {otherGameTime.GameStarted}");
|
|
||||||
|
|
||||||
diffStr = !diffs.Any() ? "No diffs" : $"\n\t{string.Join("\n\t", diffs)}";
|
|
||||||
return diffs.Any();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UpdateGameInfo(GameTime gameTime)
|
|
||||||
{
|
|
||||||
if (this.GameTimes.Any(gt => gt.appid == gameTime.appid))
|
|
||||||
{
|
|
||||||
GameTime thisGameTime = this.GameTimes.First(gt => gt.appid == gameTime.appid);
|
|
||||||
this.GameTimes.Remove(thisGameTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.GameTimes.Add(gameTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void GetGames()
|
|
||||||
{
|
|
||||||
JObject? gamesResult = net.MakeRequestGetJObject($"https://api.steampowered.com/IPlayerService/GetOwnedGames/v0001/?&steamid={steamid}&include_appinfo=true&include_played_free_games=true");
|
|
||||||
if (gamesResult is not null && gamesResult.TryGetValue("response", out JToken? value) && value.SelectToken("games") is not null)
|
|
||||||
{
|
|
||||||
foreach (JToken game in value["games"]!)
|
|
||||||
{
|
|
||||||
GameTime gameTime = game.ToObject<GameTime>();
|
|
||||||
this.UpdateGameInfo(gameTime);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Player WithInfo(Player other)
|
|
||||||
{
|
|
||||||
if (other.gameid is not null && this.gameid is null) //Game started
|
|
||||||
{
|
|
||||||
if(!this.GameTimes.Any(game => game.appid.ToString().Equals(other.gameid)))
|
|
||||||
GetGames();
|
|
||||||
GameTime gameTime = this.GameTimes.First(game => game.appid.ToString().Equals(other.gameid));
|
|
||||||
this.GameTimes.Remove(gameTime);
|
|
||||||
gameTime.GameStarted = DateTime.UtcNow;
|
|
||||||
this.GameTimes.Add(gameTime);
|
|
||||||
}else if (other.gameid is null && this.gameid is not null) //Game stopped
|
|
||||||
{
|
|
||||||
if(!this.GameTimes.Any(game => game.appid.ToString().Equals(other.gameid)))
|
|
||||||
GetGames();
|
|
||||||
GameTime gameTime = this.GameTimes.First(game => game.appid.ToString().Equals(other.gameid));
|
|
||||||
this.Sessions.Add(Session.FromGameTime(gameTime));
|
|
||||||
this.GameTimes.Remove(gameTime);
|
|
||||||
gameTime.GameStarted = null;
|
|
||||||
this.GameTimes.Add(gameTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this with
|
|
||||||
{
|
|
||||||
personastate = other.personastate,
|
|
||||||
communityvisibilitystate = other.communityvisibilitystate,
|
|
||||||
profilestate = other.profilestate,
|
|
||||||
lastlogoff = other.lastlogoff,
|
|
||||||
commentpermission = other.commentpermission,
|
|
||||||
realname = other.realname,
|
|
||||||
primaryclanid = other.primaryclanid,
|
|
||||||
gameserverip = other.gameserverip,
|
|
||||||
gameextrainfo = other.gameextrainfo,
|
|
||||||
loccountrycode = other.loccountrycode,
|
|
||||||
locstatecode = other.locstatecode,
|
|
||||||
loccityid = other.loccityid,
|
|
||||||
personaname = other.personaname,
|
|
||||||
profileurl = other.profileurl,
|
|
||||||
avatar = other.avatar,
|
|
||||||
avatarmedium = other.avatarmedium,
|
|
||||||
avatarfull = other.avatarfull,
|
|
||||||
gameid = other.gameid
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void ShuttingDownSaveSessions()
|
|
||||||
{
|
|
||||||
if (this.gameid is null)
|
|
||||||
return;
|
|
||||||
string gid = this.gameid;
|
|
||||||
GameTime? gameTime = this.GameTimes.FirstOrDefault(game => game.appid.ToString().Equals(gid));
|
|
||||||
if (gameTime is null || gameTime.Value.GameStarted is null)
|
|
||||||
return;
|
|
||||||
this.Sessions.Add(Session.FromGameTime(gameTime.Value));
|
|
||||||
this.gameid = null;
|
|
||||||
this.Export();
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class PlayerJsonConverter : JsonConverter
|
|
||||||
{
|
|
||||||
private Net net;
|
|
||||||
|
|
||||||
public PlayerJsonConverter(Net net)
|
|
||||||
{
|
|
||||||
this.net = net;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool CanConvert(Type objectType)
|
|
||||||
{
|
|
||||||
return objectType == typeof(Player);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
|
||||||
{
|
|
||||||
JObject jo = JObject.Load(reader);
|
|
||||||
Player tmp = new (jo.Value<string>("steamid")!, net, jo.Value<bool>("IsMe"))
|
|
||||||
{
|
|
||||||
GameTimes = jo["GameTimes"]!.ToObject<HashSet<GameTime>>()!,
|
|
||||||
Sessions = jo["Sessions"]!.ToObject<List<Session>>()!
|
|
||||||
};
|
|
||||||
return tmp.WithInfo(jo.ToObject<Player>());
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool CanWrite => false;
|
|
||||||
|
|
||||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum PersonaStateEnum : byte
|
|
||||||
{
|
|
||||||
Offline = 0,
|
|
||||||
Online = 1,
|
|
||||||
Busy = 2,
|
|
||||||
Away = 3,
|
|
||||||
Snooze = 4,
|
|
||||||
LookingToTrade = 5,
|
|
||||||
LookingToPlay = 6
|
|
||||||
}
|
|
@ -1,65 +0,0 @@
|
|||||||
// See https://aka.ms/new-console-template for more information
|
|
||||||
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using SteamGameTimeTrack;
|
|
||||||
|
|
||||||
Config config;
|
|
||||||
|
|
||||||
if (File.Exists("config.json"))
|
|
||||||
config = JsonConvert.DeserializeObject<Config>(File.ReadAllText("config.json"));
|
|
||||||
else
|
|
||||||
{
|
|
||||||
string? steamid;
|
|
||||||
do
|
|
||||||
{
|
|
||||||
Console.Clear();
|
|
||||||
Console.WriteLine("SteamId:");
|
|
||||||
steamid = Console.ReadLine();
|
|
||||||
} while (steamid is null || steamid.Length < 1);
|
|
||||||
|
|
||||||
string? apiKey;
|
|
||||||
do
|
|
||||||
{
|
|
||||||
Console.Clear();
|
|
||||||
Console.WriteLine("API-Key:");
|
|
||||||
apiKey = Console.ReadLine();
|
|
||||||
} while (apiKey is null || apiKey.Length < 1);
|
|
||||||
|
|
||||||
LogLevel logLevel;
|
|
||||||
string[] levels = Enum.GetNames<LogLevel>();
|
|
||||||
do
|
|
||||||
{
|
|
||||||
Console.Clear();
|
|
||||||
for (int i = 0; i < levels.Length; i++)
|
|
||||||
Console.WriteLine($"{i}) {levels[i]}");
|
|
||||||
} while (!int.TryParse(Console.ReadKey().KeyChar.ToString(), out int selectedLevel) || selectedLevel < 0 ||
|
|
||||||
selectedLevel >= levels.Length || !Enum.TryParse<LogLevel>(levels[selectedLevel], out logLevel));
|
|
||||||
|
|
||||||
Console.Clear();
|
|
||||||
Console.WriteLine("Track friends? (y/n)");
|
|
||||||
bool trackFriends = Console.ReadKey(true).Key == ConsoleKey.Y;
|
|
||||||
|
|
||||||
int port;
|
|
||||||
do
|
|
||||||
{
|
|
||||||
Console.Clear();
|
|
||||||
Console.WriteLine("API-Port:");
|
|
||||||
} while (!int.TryParse(Console.ReadLine(), out port) || port < 1 || port > 65535);
|
|
||||||
|
|
||||||
config = new()
|
|
||||||
{
|
|
||||||
SteamId = steamid,
|
|
||||||
ApiToken = apiKey,
|
|
||||||
LogLevel = logLevel,
|
|
||||||
TrackFriends = trackFriends,
|
|
||||||
ApiPort = port
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Tracker t = new (config);
|
|
||||||
while(Console.ReadKey(true).Key != ConsoleKey.Q)
|
|
||||||
Console.WriteLine("Press q to quit.");
|
|
||||||
t.Dispose();
|
|
@ -1,25 +0,0 @@
|
|||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace SteamGameTimeTrack;
|
|
||||||
|
|
||||||
public struct Session
|
|
||||||
{
|
|
||||||
public int AppId;
|
|
||||||
public string Name;
|
|
||||||
public DateTime SessionStarted;
|
|
||||||
public TimeSpan SessionDuration;
|
|
||||||
[JsonIgnore] public DateTime SessionEnded => SessionStarted.Add(SessionDuration);
|
|
||||||
|
|
||||||
public static Session FromGameTime(GameTime gameTime)
|
|
||||||
{
|
|
||||||
if (gameTime.GameStarted is null)
|
|
||||||
throw new ArgumentNullException(nameof(GameTime.GameStarted));
|
|
||||||
return new Session()
|
|
||||||
{
|
|
||||||
AppId = gameTime.appid,
|
|
||||||
Name = gameTime.name,
|
|
||||||
SessionStarted = gameTime.GameStarted.Value,
|
|
||||||
SessionDuration = gameTime.Session!.Value
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<OutputType>Exe</OutputType>
|
|
||||||
<TargetFramework>net7.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Content Include="..\.dockerignore">
|
|
||||||
<Link>.dockerignore</Link>
|
|
||||||
</Content>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="GlaxLogger" Version="1.0.7.2" />
|
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
@ -1,201 +0,0 @@
|
|||||||
using GlaxLogger;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
|
|
||||||
namespace SteamGameTimeTrack;
|
|
||||||
|
|
||||||
public class Tracker : IDisposable
|
|
||||||
{
|
|
||||||
private bool _running = true;
|
|
||||||
// ReSharper disable once MemberInitializerValueIgnored
|
|
||||||
private readonly ILogger _logger = null!;
|
|
||||||
internal readonly HashSet<Player> _players = new();
|
|
||||||
private readonly Net net;
|
|
||||||
private readonly Config config;
|
|
||||||
private readonly API api;
|
|
||||||
|
|
||||||
private readonly TimeSpan _updateFriendsInterval = TimeSpan.FromMinutes(30);
|
|
||||||
private readonly TimeSpan _updateGamesInterval = TimeSpan.FromMinutes(60);
|
|
||||||
private readonly TimeSpan _updateInfoInterval = TimeSpan.FromSeconds(60);
|
|
||||||
private readonly Thread _updateFriendsThread, _updateGamesThread, _updateInfoThread;
|
|
||||||
|
|
||||||
public Tracker(Config config)
|
|
||||||
{
|
|
||||||
this.config = config;
|
|
||||||
this._logger = new Logger(config.LogLevel, consoleOut: Console.Out);
|
|
||||||
this.net = new(this._logger, config.ApiToken);
|
|
||||||
this._logger.LogInformation(config.ToString());
|
|
||||||
|
|
||||||
LoadKnownInformation();
|
|
||||||
if(!_players.Any(player => player.IsMe))
|
|
||||||
_players.Add(new Player(config.SteamId, net, true));
|
|
||||||
|
|
||||||
if (config.TrackFriends)
|
|
||||||
UpdateFriends();
|
|
||||||
|
|
||||||
UpdatePlayerGames();
|
|
||||||
UpdatePlayerInfo();
|
|
||||||
|
|
||||||
this._updateFriendsThread = new (() =>
|
|
||||||
{
|
|
||||||
lock(_updateFriendsThread!)
|
|
||||||
{
|
|
||||||
while (_running)
|
|
||||||
{
|
|
||||||
_logger.LogInformation($"Waiting {_updateFriendsInterval:g} for next Friends-List-Update. {DateTime.Now.Add(_updateFriendsInterval):HH:mm:ss zz}");
|
|
||||||
Monitor.Wait(_updateFriendsThread, _updateFriendsInterval);
|
|
||||||
if (!_running)
|
|
||||||
break;
|
|
||||||
UpdateFriends();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this._updateFriendsThread.Start();
|
|
||||||
|
|
||||||
this._updateGamesThread = new(() =>
|
|
||||||
{
|
|
||||||
lock(_updateGamesThread!)
|
|
||||||
{
|
|
||||||
while (_running)
|
|
||||||
{
|
|
||||||
_logger.LogInformation($"Waiting {_updateGamesInterval:g} for next Player-Games-Update. {DateTime.Now.Add(_updateGamesInterval):HH:mm:ss zz}");
|
|
||||||
Monitor.Wait(_updateGamesThread, _updateGamesInterval);
|
|
||||||
if (!_running)
|
|
||||||
break;
|
|
||||||
UpdatePlayerGames();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this._updateGamesThread.Start();
|
|
||||||
|
|
||||||
this._updateInfoThread = new(() =>
|
|
||||||
{
|
|
||||||
lock(_updateInfoThread!)
|
|
||||||
{
|
|
||||||
while (_running)
|
|
||||||
{
|
|
||||||
_logger.LogInformation($"Waiting {_updateInfoInterval:g} for next Player-Info-Update. {DateTime.Now.Add(_updateInfoInterval):HH:mm:ss zz}");
|
|
||||||
Monitor.Wait(_updateInfoThread, _updateInfoInterval);
|
|
||||||
if (!_running)
|
|
||||||
break;
|
|
||||||
UpdatePlayerInfo();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this._updateInfoThread.Start();
|
|
||||||
|
|
||||||
this.api = new(this, config.ApiPort, this._logger);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Tracker(string steamId, string apiToken, bool trackFriends = true, int apiPort = 8888, LogLevel logLevel = LogLevel.Information) : this(new Config()
|
|
||||||
{
|
|
||||||
ApiToken = apiToken,
|
|
||||||
LogLevel = logLevel,
|
|
||||||
SteamId = steamId,
|
|
||||||
TrackFriends = trackFriends,
|
|
||||||
ApiPort = apiPort
|
|
||||||
})
|
|
||||||
{
|
|
||||||
File.WriteAllText("config.json", JsonConvert.SerializeObject(config, Formatting.Indented));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateFriends()
|
|
||||||
{
|
|
||||||
this._logger.LogInformation("Getting Friends-List...");
|
|
||||||
JObject? friendsResult = net.MakeRequestGetJObject($"https://api.steampowered.com/ISteamUser/GetFriendList/v0001/?steamid={config.SteamId}&relationship=friend");
|
|
||||||
if (friendsResult is not null && friendsResult.TryGetValue("friendslist", out JToken? value) && value.SelectToken("friends") is not null)
|
|
||||||
{
|
|
||||||
this._logger.LogDebug("Got Friends-List.");
|
|
||||||
foreach (JToken friend in value["friends"]!)
|
|
||||||
{
|
|
||||||
string steamid = friend["steamid"]!.Value<string>()!;
|
|
||||||
if(!_players.Any(player => player.steamid.Equals(steamid)))
|
|
||||||
this._players.Add(new(steamid, net)
|
|
||||||
{
|
|
||||||
friend_since = friend["friend_since"]!.Value<long>()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this._logger.LogInformation($"Got {this._players.Count - 1} friends.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdatePlayerGames()
|
|
||||||
{
|
|
||||||
int i = 1;
|
|
||||||
foreach (Player player in this._players)
|
|
||||||
{
|
|
||||||
_logger.LogInformation($"Getting GameInfo {i++}/{_players.Count} {player.personaname} {player.steamid}");
|
|
||||||
player.GetGames();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdatePlayerInfo()
|
|
||||||
{
|
|
||||||
this._logger.LogInformation("Getting Player-info...");
|
|
||||||
string steamIds = string.Join(',', _players.Select(player => player.steamid));
|
|
||||||
JObject? result = net.MakeRequestGetJObject($"https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?steamids={steamIds}");
|
|
||||||
if (result is not null && result.TryGetValue("response", out JToken? response))
|
|
||||||
{
|
|
||||||
foreach (JToken player in response["players"]!)
|
|
||||||
{
|
|
||||||
Player p = player.ToObject<Player>();
|
|
||||||
UpdatePlayer(p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this._logger.LogInformation("Done getting Player-info...");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void LoadKnownInformation()
|
|
||||||
{
|
|
||||||
if (!Directory.Exists("states"))
|
|
||||||
return;
|
|
||||||
foreach (DirectoryInfo subDir in new DirectoryInfo("states").GetDirectories())
|
|
||||||
{
|
|
||||||
FileInfo? latest = subDir.GetFiles().MaxBy(file => file.CreationTimeUtc);
|
|
||||||
if (latest is not null)
|
|
||||||
{
|
|
||||||
Player player = JsonConvert.DeserializeObject<Player>(File.ReadAllText(latest.FullName), new Player.PlayerJsonConverter(net));
|
|
||||||
this._players.Add(player);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdatePlayer(Player player)
|
|
||||||
{
|
|
||||||
this._logger.LogDebug($"Updating Player {player.personaname} {player.steamid}");
|
|
||||||
Player p = this._players.First(pp => pp.steamid.Equals(player.steamid));
|
|
||||||
this._players.Remove(p);
|
|
||||||
Player updated = p.WithInfo(player);
|
|
||||||
|
|
||||||
this._players.Add(updated);
|
|
||||||
if (p.Diffs(updated, out string diffStr))
|
|
||||||
{
|
|
||||||
updated.Export();
|
|
||||||
this._logger.LogDebug($"Diffs: {diffStr}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
this._logger.LogInformation("Stopping...");
|
|
||||||
this.api.Dispose();
|
|
||||||
_running = false;
|
|
||||||
lock (_updateInfoThread)
|
|
||||||
{
|
|
||||||
Monitor.Pulse(_updateInfoThread);
|
|
||||||
}
|
|
||||||
lock (_updateGamesThread)
|
|
||||||
{
|
|
||||||
Monitor.Pulse(_updateGamesThread);
|
|
||||||
}
|
|
||||||
lock (_updateFriendsThread)
|
|
||||||
{
|
|
||||||
Monitor.Pulse(_updateFriendsThread);
|
|
||||||
}
|
|
||||||
this._logger.LogInformation("Exporting Player-States...");
|
|
||||||
foreach (Player player in this._players)
|
|
||||||
player.ShuttingDownSaveSessions();
|
|
||||||
this._logger.LogInformation("Done. Bye.");
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user