Browse Source

Add leaderboard submission support

tags/v1.0.0
NGnius (Graham) 4 years ago
parent
commit
d34f484ed1
13 changed files with 946 additions and 427 deletions
  1. +484
    -368
      Leadercraft/Leadercraft.csproj
  2. +48
    -4
      Leadercraft/LeadercraftPlugin.cs
  3. +20
    -0
      Leadercraft/Scoring/GameEventHandlers.cs
  4. +56
    -0
      Leadercraft/Scoring/GameLoop.cs
  5. +87
    -0
      Leadercraft/Scoring/State.cs
  6. +83
    -0
      Leadercraft/Scoring/UploadJob.cs
  7. +4
    -2
      Leadercraft/Server/CriteriaStruct.cs
  8. +4
    -1
      Leadercraft/Server/KeyStruct.cs
  9. +72
    -40
      Leadercraft/Server/LeadercraftApi.cs
  10. +3
    -1
      Leadercraft/Server/NewKeyStruct.cs
  11. +3
    -0
      Leadercraft/Server/ResultStruct.cs
  12. +69
    -9
      Leadercraft/Server/Tests.cs
  13. +13
    -2
      Leadercraft/Server/Tools.cs

+ 484
- 368
Leadercraft/Leadercraft.csproj
File diff suppressed because it is too large
View File


+ 48
- 4
Leadercraft/LeadercraftPlugin.cs View File

@@ -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";
}
}
}

+ 20
- 0
Leadercraft/Scoring/GameEventHandlers.cs View File

@@ -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")
{

}
}
}

+ 56
- 0
Leadercraft/Scoring/GameLoop.cs View File

@@ -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();
}
}
}
}
}
}

+ 87
- 0
Leadercraft/Scoring/State.cs View File

@@ -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,
};
}
}
}

+ 83
- 0
Leadercraft/Scoring/UploadJob.cs View File

@@ -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}");
}
}
}

+ 4
- 2
Leadercraft/Server/CriteriaStruct.cs View File

@@ -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;
}
}

+ 4
- 1
Leadercraft/Server/KeyStruct.cs View File

@@ -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
}

+ 72
- 40
Leadercraft/Server/LeadercraftApi.cs View File

@@ -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
- 1
Leadercraft/Server/NewKeyStruct.cs View File

@@ -3,6 +3,8 @@ namespace Leadercraft.Server
{
internal struct NewKeyStruct
{
public uint PlayerID;
public ulong PlayerID;

public string PlayerName;
}
}

+ 3
- 0
Leadercraft/Server/ResultStruct.cs View File

@@ -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
}

+ 69
- 9
Leadercraft/Server/Tests.cs View File

@@ -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
}

+ 13
- 2
Leadercraft/Server/Tools.cs View File

@@ -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;
}
}
}