@@ -1,17 +1,50 @@ | |||
using System.Reflection; | |||
using IllusionPlugin; | |||
using GamecraftModdingAPI; | |||
using GamecraftModdingAPI.Utility; | |||
using Leadercraft.Server; | |||
namespace Leadercraft | |||
{ | |||
public class LeadercraftPlugin : IPlugin // the Illusion Plugin Architecture (IPA) will ignore classes that don't implement IPlugin' | |||
{ | |||
public static float LoopDelay = 0.1f; | |||
public string Name { get; } = Assembly.GetExecutingAssembly().GetName().Name; // mod name | |||
public string Version { get; } = Assembly.GetExecutingAssembly().GetName().Version.ToString(); // mod & assembly version | |||
// called when Gamecraft shuts down | |||
internal static LeadercraftApi Api = null; | |||
private static string tokenUrl = | |||
#if DEBUG | |||
"http://192.168.122.229:1337/token"; | |||
#else | |||
"https://leadercraft.exmods.org/token"; | |||
#endif | |||
private static string criteriaUrl = | |||
#if DEBUG | |||
"http://192.168.122.229:7048/criteria"; | |||
#else | |||
"https://board.exmods.org/criteria"; | |||
#endif | |||
public static void BuildApi() | |||
{ | |||
if (Api == null) | |||
{ | |||
if (!Tools.IsSteamAvailable) | |||
{ | |||
Logging.MetaDebugLog("Steam is unavailable :("); | |||
return; | |||
} | |||
Api = new LeadercraftApi(Tools.UserId, tokenUrl, criteriaUrl); | |||
GamecraftModdingAPI.Utility.Logging.MetaDebugLog("Leadercraft API initialized"); | |||
} | |||
} | |||
// called when Gamecraft shuts down | |||
public void OnApplicationQuit() | |||
{ | |||
// Shutdown this mod | |||
@@ -29,18 +62,29 @@ namespace Leadercraft | |||
// check out the modding API docs here: https://mod.exmods.org/ | |||
// Initialize this mod | |||
GamecraftModdingAPI.Utility.GameEngineManager.AddGameEngine(new Scoring.LeadercraftSimEventHandler()); | |||
GamecraftModdingAPI.Utility.GameEngineManager.AddGameEngine(new Scoring.LeadercraftGameEventHandler()); | |||
GamecraftModdingAPI.Events.EventManager.AddEventHandler(new Scoring.GameLoop()); | |||
GamecraftModdingAPI.Utility.Logging.LogDebug($"{Name} has started up"); | |||
// Debug mode | |||
Debug(); | |||
} | |||
// unused methods | |||
public void OnFixedUpdate() { } // called once per physics update | |||
public void OnLevelWasInitialized(int level) { } // called after a level is initialized | |||
public void OnLevelWasInitialized(int level) { }// called after a level is initialized | |||
public void OnLevelWasLoaded(int level) { } // called after a level is loaded | |||
public void OnUpdate() { } // called once per rendered frame (frame update) | |||
public static void Debug() | |||
{ | |||
tokenUrl = "http://192.168.122.229:1337/token"; | |||
criteriaUrl = "http://192.168.122.229:7048/criteria"; | |||
} | |||
} | |||
} |
@@ -0,0 +1,20 @@ | |||
using System; | |||
using GamecraftModdingAPI.Events; | |||
namespace Leadercraft.Scoring | |||
{ | |||
internal class LeadercraftGameEventHandler : SimpleEventHandlerEngine | |||
{ | |||
public LeadercraftGameEventHandler(): base(State.EnterGame, State.ExitGame, EventType.GameSwitchedTo, "LeadercraftGameEventHandler") | |||
{ | |||
} | |||
} | |||
internal class LeadercraftSimEventHandler : SimpleEventHandlerEngine | |||
{ | |||
public LeadercraftSimEventHandler() : base(State.StartPlayingGame, State.StopPlayingGame, EventType.SimulationSwitchedTo, "LeadercraftSimEventHandler") | |||
{ | |||
} | |||
} | |||
} |
@@ -0,0 +1,56 @@ | |||
using System; | |||
using RobocraftX.Common; | |||
using Gamecraft.Blocks.HUDFeedbackBlocks; | |||
using GamecraftModdingAPI; | |||
using GamecraftModdingAPI.Engines; | |||
using GamecraftModdingAPI.Events; | |||
using GamecraftModdingAPI.Players; | |||
using GamecraftModdingAPI.Tasks; | |||
using GamecraftModdingAPI.Utility; | |||
using Svelto.ECS; | |||
using Unity.Jobs; | |||
using Leadercraft.Server; | |||
namespace Leadercraft.Scoring | |||
{ | |||
internal class GameLoop : SimpleEventHandlerEngine | |||
{ | |||
private static Player localPlayer; | |||
public GameLoop() : base (OnGameEnter, (_) => {}, EventType.GameSwitchedTo, "LeadercraftGameLoopGameEngine") { } | |||
public static void OnGameEnter(EntitiesDB entitiesDB) | |||
{ | |||
// schedule game loop async task | |||
Action loop = () => { loopPass(entitiesDB); }; | |||
ISchedulable looper = new Repeatable(loop, () => { return State.IsInGame; }, LeadercraftPlugin.LoopDelay); | |||
Scheduler.Schedule(looper); | |||
} | |||
private static void loopPass(EntitiesDB entitiesDB) | |||
{ | |||
if (localPlayer == null) localPlayer = new Player(PlayerType.Local); | |||
if (!State.IsPlayingGame) return; | |||
FilteredChannelDataStruct[] channelInfo = entitiesDB.QueryEntities<FilteredChannelDataStruct>(CommonExclusiveGroups.OWNED_BLOCKS_GROUP).ToFastAccess(out uint count); | |||
for (uint i = 0; i < count; i++) | |||
{ | |||
FilteredChannelDataStruct data = channelInfo[i]; | |||
if(data.channelSignals.any && entitiesDB.Exists<GameOverHudBlockEntityStruct>(data.ID)) | |||
{ | |||
GameOverHudTextEntityStruct hudText = entitiesDB.QueryEntity<GameOverHudTextEntityStruct>(data.ID); | |||
if (((string)hudText.headerText).ToLower().Contains("win") || ((string)hudText.bodyText).ToLower().Contains("win")) | |||
{ | |||
State.StopPlayingGame(); | |||
//State.GamePlayTime.TotalSeconds | |||
UploadJob scoreJob = new UploadJob(State.Score, State.GamePlayTime.TotalSeconds, localPlayer.Position, Tools.UserId, Tools.UserName, Tools.GameId); | |||
scoreJob.RunInNewThread(); | |||
} | |||
} | |||
} | |||
} | |||
} | |||
} |
@@ -0,0 +1,87 @@ | |||
using System; | |||
using GamecraftModdingAPI.Utility; | |||
namespace Leadercraft.Scoring | |||
{ | |||
internal static class State | |||
{ | |||
public static bool IsInGame { get; private set; }= false; | |||
public static bool IsPlayingGame { get; private set; } = false; | |||
public static DateTime GameEnterTime { get; private set; } | |||
public static DateTime GameStartTime { get; private set; } | |||
public static TimeSpan GamePlayTime { get; private set; } | |||
public static float[] PlayerLocation { get; private set; } | |||
public static ulong GameId { get; private set; } | |||
public static int Score { get; private set; } | |||
public static bool IsGameComplete { get; private set; } | |||
public static bool IsGameSynced { get; private set; } | |||
public static void EnterGame() | |||
{ | |||
if (IsInGame) return; | |||
Logging.MetaDebugLog("Entering game"); | |||
IsInGame = true; | |||
IsGameSynced = false; | |||
IsGameComplete = false; | |||
GameId = Server.Tools.GameId; | |||
GameEnterTime = DateTime.UtcNow; | |||
} | |||
public static void ExitGame() | |||
{ | |||
if (!IsInGame) return; | |||
Logging.MetaDebugLog("Exiting game"); | |||
IsInGame = false; | |||
} | |||
public static void StartPlayingGame() | |||
{ | |||
if (IsPlayingGame) return; | |||
Logging.MetaDebugLog("Starting to play game"); | |||
Score = 0; | |||
IsPlayingGame = true; | |||
GameStartTime = DateTime.UtcNow; | |||
} | |||
public static void StopPlayingGame() | |||
{ | |||
if (!IsPlayingGame) return; | |||
GamePlayTime = DateTime.UtcNow - GameStartTime; | |||
Logging.MetaDebugLog("Stopping game"); | |||
IsPlayingGame = false; | |||
} | |||
public static void SetLocation(float x, float y, float z) | |||
{ | |||
PlayerLocation = new float[] { x, y, z }; | |||
} | |||
public static void AddScore(int points) | |||
{ | |||
Score += points; | |||
} | |||
public static Server.CriteriaStruct Criteria() | |||
{ | |||
IsGameSynced = true; | |||
return new Server.CriteriaStruct | |||
{ | |||
Location = new float[][] { PlayerLocation, PlayerLocation }, | |||
Time = Convert.ToInt32(Math.Round(GamePlayTime.TotalSeconds, MidpointRounding.AwayFromZero)), | |||
GameID = GameId, | |||
PlayerID = 0, | |||
Complete = IsGameComplete, | |||
Points = Score, | |||
}; | |||
} | |||
} | |||
} |
@@ -0,0 +1,83 @@ | |||
using System; | |||
using System.Threading; | |||
using Unity.Collections; | |||
using Unity.Jobs; | |||
using Unity.Mathematics; | |||
using GamecraftModdingAPI.Utility; | |||
using Leadercraft.Server; | |||
namespace Leadercraft.Scoring | |||
{ | |||
public struct UploadJob | |||
{ | |||
private int points; | |||
private int time; | |||
private float[] location; | |||
private ulong player; | |||
private string playerName; | |||
private ulong game; | |||
public UploadJob(int score, double timespan, float3 position, ulong playerId, string playerName, ulong gameId) | |||
{ | |||
this.points = score; | |||
this.time = (int)Math.Ceiling(timespan); | |||
this.location = new float[3] { position.x, position.y, position.z}; | |||
this.player = playerId; | |||
this.playerName = playerName; | |||
this.game = 2;//gameId; | |||
LeadercraftPlugin.BuildApi(); | |||
} | |||
public void RunInNewThread() | |||
{ | |||
Thread job = new Thread(new ThreadStart(Execute)); | |||
job.Start(); | |||
} | |||
private void Execute() | |||
{ | |||
if (player < 1000 || game == 0) return; // offline game | |||
LeadercraftResult<KeyStruct> tokenResult = LeadercraftPlugin.Api.RequestPOSTToken(playerName); | |||
if (tokenResult.IsError) | |||
{ | |||
Logging.LogWarning($"Leadercraft API token web request failed with status {tokenResult.StatusCode}"); | |||
try | |||
{ | |||
Logging.LogWarning($"API Error Response: {tokenResult.ParseError().Items[0]}"); | |||
} | |||
catch (Exception) { } | |||
return; | |||
} | |||
string token = tokenResult.ParseResult().Items[0].Token; | |||
CriteriaStruct criteria = new CriteriaStruct | |||
{ | |||
Location = new float[][] { location, location }, | |||
Time = time, | |||
GameID = game, | |||
PlayerID = player, | |||
Complete = true, | |||
Points = points | |||
}; | |||
LeadercraftResult<string> criteriaResult = LeadercraftPlugin.Api.RequestPOSTCriteria(criteria, token); | |||
if (criteriaResult.IsError) | |||
{ | |||
Logging.LogWarning($"Leadercraft API criteria web request failed with status {criteriaResult.StatusCode}"); | |||
try | |||
{ | |||
Logging.LogWarning($"API Error Response: {criteriaResult.ParseError().Items[0]}"); | |||
} | |||
catch (Exception) { } | |||
return; | |||
} | |||
Logging.MetaLog($"Criteria request succeeded with status {criteriaResult.StatusCode}"); | |||
} | |||
} | |||
} |
@@ -7,10 +7,12 @@ namespace Leadercraft.Server | |||
public int Time; // time since start of game (seconds) | |||
public int GameID; | |||
public ulong GameID; | |||
public int PlayerID; | |||
public ulong PlayerID; | |||
public bool Complete; | |||
public int Points; | |||
} | |||
} |
@@ -1,10 +1,13 @@ | |||
using System; | |||
namespace Leadercraft.Server | |||
{ | |||
// Json Serialisation struct | |||
#pragma warning disable 0649 | |||
internal struct KeyStruct | |||
{ | |||
public string Token; | |||
public int PlayerID; | |||
public ulong PlayerID; | |||
} | |||
#pragma warning restore 0649 | |||
} |
@@ -7,60 +7,92 @@ namespace Leadercraft.Server | |||
{ | |||
internal class LeadercraftApi | |||
{ | |||
private readonly uint _userId; | |||
private readonly ulong _userId; | |||
private readonly string _tokenUrl; | |||
private readonly string _tokenUrl; | |||
private readonly string _criteriaUrl; | |||
private readonly string _criteriaUrl; | |||
public LeadercraftApi(uint userId, string tokenUrl, string criteriaUrl) | |||
public LeadercraftApi(ulong userId, string tokenUrl, string criteriaUrl) | |||
{ | |||
this._userId = userId; | |||
this._tokenUrl = tokenUrl; | |||
this._criteriaUrl = criteriaUrl; | |||
this._userId = userId; | |||
this._tokenUrl = tokenUrl; | |||
this._criteriaUrl = criteriaUrl; | |||
} | |||
public LeadercraftResult<KeyStruct> RequestPOSTToken() | |||
{ | |||
NewKeyStruct reqBodyObj = new NewKeyStruct{ PlayerID = _userId }; | |||
byte[] reqBodyBytes = Encoding.ASCII.GetBytes(JsonConvert.SerializeObject(reqBodyObj)); | |||
public LeadercraftResult<KeyStruct> RequestPOSTToken(string playerName = "???") | |||
{ | |||
NewKeyStruct reqBodyObj = new NewKeyStruct{ PlayerID = _userId, PlayerName = playerName }; | |||
byte[] reqBodyBytes = Encoding.ASCII.GetBytes(JsonConvert.SerializeObject(reqBodyObj)); | |||
// Request | |||
WebRequest request = WebRequest.Create(_tokenUrl); | |||
request.Method = "POST"; | |||
request.ContentLength = reqBodyBytes.Length; | |||
request.ContentType = "application/json"; | |||
Stream body = request.GetRequestStream(); | |||
body.Write(reqBodyBytes, 0, reqBodyBytes.Length); | |||
body.Close(); | |||
request.Method = "POST"; | |||
request.ContentLength = reqBodyBytes.Length; | |||
request.ContentType = "application/json"; | |||
Stream body; | |||
try | |||
{ | |||
body = request.GetRequestStream(); | |||
body.Write(reqBodyBytes, 0, reqBodyBytes.Length); | |||
body.Close(); | |||
} | |||
catch (WebException e) | |||
{ | |||
return new LeadercraftResult<KeyStruct>(new byte[] { }, (int)e.Status); | |||
} | |||
// Response | |||
HttpWebResponse response = (HttpWebResponse)request.GetResponse(); | |||
body = response.GetResponseStream(); | |||
byte[] respBodyBytes = new byte[int.Parse(response.GetResponseHeader("Content-Length"))]; | |||
body.Read(respBodyBytes, 0, int.Parse(response.GetResponseHeader("Content-Length"))); | |||
response.Close(); | |||
return new LeadercraftResult<KeyStruct>(respBodyBytes, (int)response.StatusCode); | |||
} | |||
HttpWebResponse response = null; | |||
try | |||
{ | |||
response = (HttpWebResponse)request.GetResponse(); | |||
} | |||
catch (WebException e) | |||
{ | |||
return new LeadercraftResult<KeyStruct>(new byte[] { }, (int)e.Status); | |||
} | |||
body = response.GetResponseStream(); | |||
byte[] respBodyBytes = new byte[int.Parse(response.GetResponseHeader("Content-Length"))]; | |||
body.Read(respBodyBytes, 0, respBodyBytes.Length); | |||
response.Close(); | |||
return new LeadercraftResult<KeyStruct>(respBodyBytes, (int)response.StatusCode); | |||
} | |||
public LeadercraftResult<string> RequestPOSTCriteria(CriteriaStruct criteria, string token) | |||
{ | |||
criteria.PlayerID = (int)_userId; | |||
byte[] reqBodyBytes = Encoding.ASCII.GetBytes(JsonConvert.SerializeObject(criteria)); | |||
// Request | |||
WebRequest request = WebRequest.Create(_criteriaUrl); | |||
public LeadercraftResult<string> RequestPOSTCriteria(CriteriaStruct criteria, string token) | |||
{ | |||
criteria.PlayerID = _userId; | |||
byte[] reqBodyBytes = Encoding.ASCII.GetBytes(JsonConvert.SerializeObject(criteria)); | |||
// Request | |||
WebRequest request = WebRequest.Create(_criteriaUrl); | |||
request.Method = "POST"; | |||
request.ContentLength = reqBodyBytes.Length; | |||
request.ContentType = "application/json"; | |||
request.Headers.Add(HttpRequestHeader.Authorization, "leadercraft "+token); | |||
Stream body = request.GetRequestStream(); | |||
body.Write(reqBodyBytes, 0, reqBodyBytes.Length); | |||
body.Close(); | |||
// Response | |||
HttpWebResponse response = (HttpWebResponse)request.GetResponse(); | |||
request.Headers.Add(HttpRequestHeader.Authorization, "leadercraft "+token); | |||
Stream body; | |||
try | |||
{ | |||
body = request.GetRequestStream(); | |||
body.Write(reqBodyBytes, 0, reqBodyBytes.Length); | |||
body.Close(); | |||
} | |||
catch (WebException e) | |||
{ | |||
return new LeadercraftResult<string>(new byte[] { }, (int)e.Status); | |||
} | |||
// Response | |||
HttpWebResponse response = null; | |||
try | |||
{ | |||
response = (HttpWebResponse)request.GetResponse(); | |||
} | |||
catch (WebException e) | |||
{ | |||
return new LeadercraftResult<string>(new byte[] { }, (int)e.Status); | |||
} | |||
body = response.GetResponseStream(); | |||
byte[] respBodyBytes = new byte[int.Parse(response.GetResponseHeader("Content-Length"))]; | |||
body.Read(respBodyBytes, 0, int.Parse(response.GetResponseHeader("Content-Length"))); | |||
byte[] respBodyBytes = new byte[int.Parse(response.GetResponseHeader("Content-Length"))]; | |||
body.Read(respBodyBytes, 0, respBodyBytes.Length); | |||
response.Close(); | |||
return new LeadercraftResult<string>(respBodyBytes, (int)response.StatusCode); | |||
} | |||
return new LeadercraftResult<string>(respBodyBytes, (int)response.StatusCode); | |||
} | |||
} | |||
} |
@@ -3,6 +3,8 @@ namespace Leadercraft.Server | |||
{ | |||
internal struct NewKeyStruct | |||
{ | |||
public uint PlayerID; | |||
public ulong PlayerID; | |||
public string PlayerName; | |||
} | |||
} |
@@ -1,6 +1,8 @@ | |||
using System; | |||
namespace Leadercraft.Server | |||
{ | |||
// Json Serialisation struct | |||
#pragma warning disable 0649 | |||
internal struct ResultStruct<T> | |||
{ | |||
public int StatusCode; // HTTP Response status code | |||
@@ -15,4 +17,5 @@ namespace Leadercraft.Server | |||
public string Start; // start time | |||
} | |||
#pragma warning restore 0649 | |||
} |
@@ -1,10 +1,13 @@ | |||
using System; | |||
# if DEBUG | |||
using NUnit.Framework; | |||
#endif | |||
namespace Leadercraft.Server | |||
{ | |||
#if DEBUG | |||
[TestFixture] | |||
public class Tests | |||
{ | |||
public class Tests | |||
{ | |||
private static readonly string tokenUrl = "http://192.168.122.229:1337/token"; | |||
private static readonly string criteriaUrl = "http://192.168.122.229:7048/criteria"; | |||
@@ -12,13 +15,13 @@ namespace Leadercraft.Server | |||
private LeadercraftApi api; | |||
[SetUp] | |||
public void SetUp() | |||
public void SetUp() | |||
{ | |||
api = new LeadercraftApi(13, tokenUrl, criteriaUrl); | |||
api = new LeadercraftApi(15, tokenUrl, criteriaUrl); | |||
} | |||
[Test] | |||
public void TokenIntegrationTest() | |||
public void TokenIntegrationTest() | |||
{ | |||
LeadercraftResult<KeyStruct> result = api.RequestPOSTToken(); | |||
Assert.AreEqual(200, result.StatusCode, "Expected HTTP 200 Ok StatusCode"); | |||
@@ -28,8 +31,8 @@ namespace Leadercraft.Server | |||
Assert.IsNotEmpty(resultStruct.Items[0].Token, "Expected a non-empty token string"); | |||
} | |||
[Test] | |||
public void CriteriaIntegrationTest() | |||
[Test] | |||
public void CriteriaIntegrationTest() | |||
{ | |||
CriteriaStruct criteria = new CriteriaStruct | |||
{ | |||
@@ -39,10 +42,67 @@ namespace Leadercraft.Server | |||
PlayerID = 333, | |||
Complete = true | |||
}; | |||
// this may fail when TokenIntegrationTest also fails | |||
// this may fail when TokenIntegrationTest also fails | |||
string token = api.RequestPOSTToken().ParseResult().Items[0].Token; | |||
LeadercraftResult<string> result = api.RequestPOSTCriteria(criteria, token); | |||
Assert.AreEqual(200, result.StatusCode, "Expected HTTP 200 Ok StatusCode"); | |||
} | |||
} | |||
[Test] | |||
public void TokenServiceUnavailableTest() | |||
{ | |||
api = new LeadercraftApi(13, "http://invalid.exmods.org:1337/token", "http://invalid.exmods.org:7048/criteria"); | |||
LeadercraftResult<KeyStruct> result = api.RequestPOSTToken(); | |||
Assert.True(result.IsError, "No error occured"); | |||
Assert.AreNotEqual(200, result.StatusCode, "Expected StatusCode other than HTTP 200 Ok"); | |||
} | |||
[Test] | |||
public void CriteriaServiceUnavailableTest() | |||
{ | |||
api = new LeadercraftApi(13, tokenUrl, "http://invalid.exmods.org:7048/criteria"); | |||
CriteriaStruct criteria = new CriteriaStruct | |||
{ | |||
Location = new float[][] { new float[] { 1, 1, 0 }, new float[] { 1, 1, 0 } }, | |||
Time = 42, | |||
GameID = 2, | |||
PlayerID = 333, | |||
Complete = true | |||
}; | |||
// this may fail when TokenIntegrationTest also fails | |||
string token = api.RequestPOSTToken().ParseResult().Items[0].Token; | |||
LeadercraftResult<string> result = api.RequestPOSTCriteria(criteria, token); | |||
Assert.True(result.IsError, "No error occured"); | |||
Assert.AreNotEqual(200, result.StatusCode, "Expected StatusCode other than HTTP 200 Ok"); | |||
} | |||
[Test] | |||
public void TokenServiceErrorTest() | |||
{ | |||
api = new LeadercraftApi(13, "http://exmods.org/wpojapowjdpoajd/token", "http://invalid.exmods.org:7048/criteria"); | |||
LeadercraftResult<KeyStruct> result = api.RequestPOSTToken(); | |||
Assert.True(result.IsError, "No error occured"); | |||
Assert.AreNotEqual(200, result.StatusCode, "Expected StatusCode other than HTTP 200 Ok"); | |||
} | |||
[Test] | |||
public void CriteriaServiceErrorTest() | |||
{ | |||
api = new LeadercraftApi(13, tokenUrl, "http://google.com/woahdoiwahdoiaw/criteria"); | |||
CriteriaStruct criteria = new CriteriaStruct | |||
{ | |||
Location = new float[][] { new float[] { 1, 1, 0 }, new float[] { 1, 1, 0 } }, | |||
Time = 42, | |||
GameID = 2, | |||
PlayerID = 333, | |||
Complete = true | |||
}; | |||
// this may fail when TokenIntegrationTest also fails | |||
string token = api.RequestPOSTToken().ParseResult().Items[0].Token; | |||
LeadercraftResult<string> result = api.RequestPOSTCriteria(criteria, token); | |||
Assert.True(result.IsError, "No error occured"); | |||
Assert.AreNotEqual(200, result.StatusCode, "Expected StatusCode other than HTTP 200 Ok"); | |||
} | |||
} | |||
#endif | |||
} |
@@ -1,5 +1,6 @@ | |||
using System; | |||
using Steamworks; | |||
using RobocraftX.Common; | |||
namespace Leadercraft.Server | |||
{ | |||
public static class Tools | |||
@@ -9,9 +10,19 @@ namespace Leadercraft.Server | |||
get => SteamClient.IsValid; | |||
} | |||
public static uint UserId | |||
public static ulong UserId | |||
{ | |||
get => SteamClient.SteamId.AccountId; | |||
get => SteamClient.SteamId.Value; | |||
} | |||
public static string UserName | |||
{ | |||
get => SteamClient.Name; | |||
} | |||
public static ulong GameId | |||
{ | |||
get => GameMode.SaveGameDetails.WorkshopId; | |||
} | |||
} | |||
} |