mirror of
https://github.com/LBPUnion/ProjectLighthouse.git
synced 2025-05-18 15:42:26 +00:00
Implement PSN ticket reading
This commit is contained in:
parent
ef84bf1d50
commit
7081b725a8
14 changed files with 266 additions and 68 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -28,6 +28,7 @@ gitVersion.txt
|
||||||
gitRemotes.txt
|
gitRemotes.txt
|
||||||
gitUnpushed.txt
|
gitUnpushed.txt
|
||||||
logs/*
|
logs/*
|
||||||
|
npTicket*
|
||||||
|
|
||||||
# MSBuild stuff
|
# MSBuild stuff
|
||||||
bin/
|
bin/
|
||||||
|
|
|
@ -37,7 +37,8 @@ public class LighthouseServerTest
|
||||||
await database.CreateUser($"{username}{number}", HashHelper.BCryptHash($"unitTestPassword{number}"));
|
await database.CreateUser($"{username}{number}", HashHelper.BCryptHash($"unitTestPassword{number}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
string stringContent = $"{LoginData.UsernamePrefix}{username}{number}{(char)0x00}";
|
//TODO: generate actual tickets
|
||||||
|
string stringContent = $"unitTestTicket{username}{number}";
|
||||||
|
|
||||||
HttpResponseMessage response = await this.Client.PostAsync
|
HttpResponseMessage response = await this.Client.PostAsync
|
||||||
($"/LITTLEBIGPLANETPS3_XML/login?titleID={GameVersionHelper.LittleBigPlanet2TitleIds[0]}", new StringContent(stringContent));
|
($"/LITTLEBIGPLANETPS3_XML/login?titleID={GameVersionHelper.LittleBigPlanet2TitleIds[0]}", new StringContent(stringContent));
|
||||||
|
|
|
@ -8,8 +8,10 @@ using LBPUnion.ProjectLighthouse.Helpers;
|
||||||
using LBPUnion.ProjectLighthouse.Logging;
|
using LBPUnion.ProjectLighthouse.Logging;
|
||||||
using LBPUnion.ProjectLighthouse.Types;
|
using LBPUnion.ProjectLighthouse.Types;
|
||||||
using LBPUnion.ProjectLighthouse.Types.Settings;
|
using LBPUnion.ProjectLighthouse.Types.Settings;
|
||||||
|
using LBPUnion.ProjectLighthouse.Types.Tickets;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using IOFile = System.IO.File;
|
||||||
|
|
||||||
namespace LBPUnion.ProjectLighthouse.Controllers;
|
namespace LBPUnion.ProjectLighthouse.Controllers;
|
||||||
|
|
||||||
|
@ -26,25 +28,29 @@ public class LoginController : ControllerBase
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
public async Task<IActionResult> Login([FromQuery] string? titleId)
|
public async Task<IActionResult> Login()
|
||||||
{
|
{
|
||||||
titleId ??= "";
|
MemoryStream ms = new();
|
||||||
|
await this.Request.Body.CopyToAsync(ms);
|
||||||
|
byte[] loginData = ms.ToArray();
|
||||||
|
|
||||||
string body = await new StreamReader(this.Request.Body).ReadToEndAsync();
|
#if DEBUG
|
||||||
|
await IOFile.WriteAllBytesAsync($"npTicket-{TimestampHelper.TimestampMillis}.txt", loginData);
|
||||||
|
#endif
|
||||||
|
|
||||||
LoginData? loginData;
|
NPTicket? npTicket;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
loginData = LoginData.CreateFromString(body);
|
npTicket = NPTicket.CreateFromBytes(loginData);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
loginData = null;
|
npTicket = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loginData == null)
|
if (npTicket == null)
|
||||||
{
|
{
|
||||||
Logger.Log("loginData was null, rejecting login", LoggerLevelLogin.Instance);
|
Logger.Log("npTicket was null, rejecting login", LoggerLevelLogin.Instance);
|
||||||
return this.BadRequest();
|
return this.BadRequest();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,11 +66,11 @@ public class LoginController : ControllerBase
|
||||||
// Get an existing token from the IP & username
|
// Get an existing token from the IP & username
|
||||||
GameToken? token = await this.database.GameTokens.Include
|
GameToken? token = await this.database.GameTokens.Include
|
||||||
(t => t.User)
|
(t => t.User)
|
||||||
.FirstOrDefaultAsync(t => t.UserLocation == ipAddress && t.User.Username == loginData.Username && !t.Used);
|
.FirstOrDefaultAsync(t => t.UserLocation == ipAddress && t.User.Username == npTicket.Username && !t.Used);
|
||||||
|
|
||||||
if (token == null) // If we cant find an existing token, try to generate a new one
|
if (token == null) // If we cant find an existing token, try to generate a new one
|
||||||
{
|
{
|
||||||
token = await this.database.AuthenticateUser(loginData, ipAddress, titleId);
|
token = await this.database.AuthenticateUser(npTicket, ipAddress);
|
||||||
if (token == null)
|
if (token == null)
|
||||||
{
|
{
|
||||||
Logger.Log("unable to find/generate a token, rejecting login", LoggerLevelLogin.Instance);
|
Logger.Log("unable to find/generate a token, rejecting login", LoggerLevelLogin.Instance);
|
||||||
|
@ -129,7 +135,7 @@ public class LoginController : ControllerBase
|
||||||
return this.StatusCode(403, "");
|
return this.StatusCode(403, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.Log($"Successfully logged in user {user.Username} as {token.GameVersion} client ({titleId})", LoggerLevelLogin.Instance);
|
Logger.Log($"Successfully logged in user {user.Username} as {token.GameVersion} client", LoggerLevelLogin.Instance);
|
||||||
// After this point we are now considering this session as logged in.
|
// After this point we are now considering this session as logged in.
|
||||||
|
|
||||||
// We just logged in with the token. Mark it as used so someone else doesnt try to use it,
|
// We just logged in with the token. Mark it as used so someone else doesnt try to use it,
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Kettu;
|
|
||||||
using LBPUnion.ProjectLighthouse.Helpers;
|
using LBPUnion.ProjectLighthouse.Helpers;
|
||||||
using LBPUnion.ProjectLighthouse.Logging;
|
|
||||||
using LBPUnion.ProjectLighthouse.Types;
|
using LBPUnion.ProjectLighthouse.Types;
|
||||||
using LBPUnion.ProjectLighthouse.Types.Categories;
|
using LBPUnion.ProjectLighthouse.Types.Categories;
|
||||||
using LBPUnion.ProjectLighthouse.Types.Levels;
|
using LBPUnion.ProjectLighthouse.Types.Levels;
|
||||||
using LBPUnion.ProjectLighthouse.Types.Profiles;
|
using LBPUnion.ProjectLighthouse.Types.Profiles;
|
||||||
using LBPUnion.ProjectLighthouse.Types.Reviews;
|
using LBPUnion.ProjectLighthouse.Types.Reviews;
|
||||||
using LBPUnion.ProjectLighthouse.Types.Settings;
|
using LBPUnion.ProjectLighthouse.Types.Settings;
|
||||||
|
using LBPUnion.ProjectLighthouse.Types.Tickets;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
@ -67,9 +66,9 @@ public class Database : DbContext
|
||||||
}
|
}
|
||||||
|
|
||||||
#nullable enable
|
#nullable enable
|
||||||
public async Task<GameToken?> AuthenticateUser(LoginData loginData, string userLocation, string titleId = "")
|
public async Task<GameToken?> AuthenticateUser(NPTicket npTicket, string userLocation)
|
||||||
{
|
{
|
||||||
User? user = await this.Users.FirstOrDefaultAsync(u => u.Username == loginData.Username);
|
User? user = await this.Users.FirstOrDefaultAsync(u => u.Username == npTicket.Username);
|
||||||
if (user == null) return null;
|
if (user == null) return null;
|
||||||
|
|
||||||
GameToken gameToken = new()
|
GameToken gameToken = new()
|
||||||
|
@ -78,15 +77,9 @@ public class Database : DbContext
|
||||||
User = user,
|
User = user,
|
||||||
UserId = user.UserId,
|
UserId = user.UserId,
|
||||||
UserLocation = userLocation,
|
UserLocation = userLocation,
|
||||||
GameVersion = GameVersionHelper.FromTitleId(titleId),
|
GameVersion = npTicket.GameVersion,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (gameToken.GameVersion == GameVersion.Unknown)
|
|
||||||
{
|
|
||||||
Logger.Log($"Unknown GameVersion for TitleId {titleId}", LoggerLevelLogin.Instance);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.GameTokens.Add(gameToken);
|
this.GameTokens.Add(gameToken);
|
||||||
await this.SaveChangesAsync();
|
await this.SaveChangesAsync();
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,10 @@ public static class BinaryReaderExtensions
|
||||||
|
|
||||||
public static int ReadInt32BE(this BinaryReader binRdr) => BitConverter.ToInt32(binRdr.ReadBytesRequired(sizeof(int)).Reverse(), 0);
|
public static int ReadInt32BE(this BinaryReader binRdr) => BitConverter.ToInt32(binRdr.ReadBytesRequired(sizeof(int)).Reverse(), 0);
|
||||||
|
|
||||||
|
public static ulong ReadUInt64BE(this BinaryReader binRdr) => BitConverter.ToUInt32(binRdr.ReadBytesRequired(sizeof(ulong)).Reverse(), 0);
|
||||||
|
|
||||||
|
public static long ReadInt64BE(this BinaryReader binRdr) => BitConverter.ToInt32(binRdr.ReadBytesRequired(sizeof(long)).Reverse(), 0);
|
||||||
|
|
||||||
public static byte[] ReadBytesRequired(this BinaryReader binRdr, int byteCount)
|
public static byte[] ReadBytesRequired(this BinaryReader binRdr, int byteCount)
|
||||||
{
|
{
|
||||||
byte[] result = binRdr.ReadBytes(byteCount);
|
byte[] result = binRdr.ReadBytes(byteCount);
|
||||||
|
|
|
@ -1,45 +0,0 @@
|
||||||
#nullable enable
|
|
||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Text;
|
|
||||||
using LBPUnion.ProjectLighthouse.Helpers;
|
|
||||||
|
|
||||||
namespace LBPUnion.ProjectLighthouse.Types;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The data sent from POST /LOGIN.
|
|
||||||
/// </summary>
|
|
||||||
public class LoginData
|
|
||||||
{
|
|
||||||
|
|
||||||
public static readonly string UsernamePrefix = Encoding.ASCII.GetString
|
|
||||||
(
|
|
||||||
new byte[]
|
|
||||||
{
|
|
||||||
0x04, 0x00, 0x20,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
public string Username { get; set; } = null!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Converts a X-I-5 Ticket into `LoginData`.
|
|
||||||
/// https://www.psdevwiki.com/ps3/X-I-5-Ticket
|
|
||||||
/// </summary>
|
|
||||||
public static LoginData? CreateFromString(string str)
|
|
||||||
{
|
|
||||||
str = str.Replace("\b", ""); // Remove backspace characters
|
|
||||||
|
|
||||||
using MemoryStream ms = new(Encoding.ASCII.GetBytes(str));
|
|
||||||
using BinaryReader reader = new(ms);
|
|
||||||
|
|
||||||
if (!str.Contains(UsernamePrefix)) return null;
|
|
||||||
|
|
||||||
LoginData loginData = new();
|
|
||||||
|
|
||||||
reader.BaseStream.Position = str.IndexOf(UsernamePrefix, StringComparison.Ordinal) + UsernamePrefix.Length;
|
|
||||||
loginData.Username = BinaryHelper.ReadString(reader).Replace("\0", string.Empty);
|
|
||||||
|
|
||||||
return loginData;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -5,4 +5,7 @@ public enum Platform
|
||||||
PS3 = 0,
|
PS3 = 0,
|
||||||
RPCS3 = 1,
|
RPCS3 = 1,
|
||||||
Vita = 2,
|
Vita = 2,
|
||||||
|
PSP = 3,
|
||||||
|
UnitTest = 4,
|
||||||
|
Unknown = -1,
|
||||||
}
|
}
|
7
ProjectLighthouse/Types/Tickets/DataHeader.cs
Normal file
7
ProjectLighthouse/Types/Tickets/DataHeader.cs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
namespace LBPUnion.ProjectLighthouse.Types.Tickets;
|
||||||
|
|
||||||
|
public struct DataHeader
|
||||||
|
{
|
||||||
|
public DataType Type;
|
||||||
|
public ushort Length;
|
||||||
|
}
|
11
ProjectLighthouse/Types/Tickets/DataType.cs
Normal file
11
ProjectLighthouse/Types/Tickets/DataType.cs
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
namespace LBPUnion.ProjectLighthouse.Types.Tickets;
|
||||||
|
|
||||||
|
public enum DataType : byte
|
||||||
|
{
|
||||||
|
Empty = 0x00,
|
||||||
|
UInt32 = 0x01,
|
||||||
|
UInt64 = 0x02,
|
||||||
|
String = 0x04,
|
||||||
|
Timestamp = 0x07,
|
||||||
|
Binary = 0x08,
|
||||||
|
}
|
125
ProjectLighthouse/Types/Tickets/NPTicket.cs
Normal file
125
ProjectLighthouse/Types/Tickets/NPTicket.cs
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
#nullable enable
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using Kettu;
|
||||||
|
using LBPUnion.ProjectLighthouse.Helpers;
|
||||||
|
using LBPUnion.ProjectLighthouse.Helpers.Extensions;
|
||||||
|
using LBPUnion.ProjectLighthouse.Logging;
|
||||||
|
using LBPUnion.ProjectLighthouse.Types.Settings;
|
||||||
|
|
||||||
|
namespace LBPUnion.ProjectLighthouse.Types.Tickets;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A PSN ticket, typically sent from PS3/RPCN
|
||||||
|
/// </summary>
|
||||||
|
public class NPTicket
|
||||||
|
{
|
||||||
|
public string Username { get; set; }
|
||||||
|
|
||||||
|
private Version ticketVersion { get; set; }
|
||||||
|
|
||||||
|
public Platform Platform { get; set; }
|
||||||
|
|
||||||
|
public uint IssuerId { get; set; }
|
||||||
|
public ulong IssuedDate { get; set; }
|
||||||
|
public ulong ExpireDate { get; set; }
|
||||||
|
|
||||||
|
public GameVersion GameVersion { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// https://www.psdevwiki.com/ps3/X-I-5-Ticket
|
||||||
|
/// </summary>
|
||||||
|
public static NPTicket? CreateFromBytes(byte[] data)
|
||||||
|
{
|
||||||
|
#if DEBUG
|
||||||
|
if (data[0] == 'u' && ServerStatics.IsUnitTesting)
|
||||||
|
{
|
||||||
|
string dataStr = Encoding.UTF8.GetString(data);
|
||||||
|
if (dataStr.StartsWith("unitTestTicket"))
|
||||||
|
{
|
||||||
|
NPTicket npTicket = new()
|
||||||
|
{
|
||||||
|
IssuerId = 0,
|
||||||
|
ticketVersion = new Version(0, 0),
|
||||||
|
Platform = Platform.UnitTest,
|
||||||
|
GameVersion = GameVersion.LittleBigPlanet2,
|
||||||
|
ExpireDate = 0,
|
||||||
|
IssuedDate = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
npTicket.Username = dataStr.Substring(14);
|
||||||
|
|
||||||
|
return npTicket;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
try
|
||||||
|
{
|
||||||
|
NPTicket npTicket = new();
|
||||||
|
using MemoryStream ms = new(data);
|
||||||
|
using TicketReader reader = new(ms);
|
||||||
|
|
||||||
|
npTicket.ticketVersion = reader.ReadTicketVersion();
|
||||||
|
|
||||||
|
reader.ReadBytes(4); // Skip header
|
||||||
|
|
||||||
|
reader.ReadUInt16BE(); // Ticket length, we don't care about this
|
||||||
|
|
||||||
|
if (npTicket.ticketVersion != "2.1") throw new NotImplementedException();
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
SectionHeader bodyHeader = reader.ReadSectionHeader();
|
||||||
|
Logger.Log($"bodyHeader.Type is {bodyHeader.Type}", LoggerLevelLogin.Instance);
|
||||||
|
#else
|
||||||
|
reader.ReadSectionHeader();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
reader.ReadTicketString(); // "Serial id", but its apparently not what we're looking for
|
||||||
|
|
||||||
|
npTicket.IssuerId = reader.ReadTicketUInt32();
|
||||||
|
npTicket.IssuedDate = reader.ReadTicketUInt64();
|
||||||
|
npTicket.ExpireDate = reader.ReadTicketUInt64();
|
||||||
|
|
||||||
|
reader.ReadTicketUInt64(); // PSN User id, we don't care about this
|
||||||
|
|
||||||
|
npTicket.Username = reader.ReadTicketString();
|
||||||
|
|
||||||
|
reader.ReadTicketString(); // Country
|
||||||
|
reader.ReadTicketString(); // Domain
|
||||||
|
|
||||||
|
// Title ID, kinda..
|
||||||
|
// Data: "UP9000-BCUS98245_00
|
||||||
|
string titleId = reader.ReadTicketString();
|
||||||
|
titleId = titleId.Substring(7); // Trim UP9000-
|
||||||
|
titleId = titleId.Substring(0, titleId.Length - 3); // Trim _00 at the end
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
Logger.Log($"titleId is {titleId}", LoggerLevelLogin.Instance);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
npTicket.GameVersion = GameVersionHelper.FromTitleId(titleId); // Finally, convert it to GameVersion
|
||||||
|
|
||||||
|
// Production PSN Issuer ID: 0x100
|
||||||
|
// RPCN Issuer ID: 0x33333333
|
||||||
|
npTicket.Platform = npTicket.IssuerId switch
|
||||||
|
{
|
||||||
|
0x100 => Platform.PS3,
|
||||||
|
0x33333333 => Platform.RPCS3,
|
||||||
|
_ => Platform.Unknown,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (npTicket.Platform == Platform.Unknown)
|
||||||
|
{
|
||||||
|
Logger.Log($"", LoggerLevelLogin.Instance);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return npTicket;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
7
ProjectLighthouse/Types/Tickets/SectionHeader.cs
Normal file
7
ProjectLighthouse/Types/Tickets/SectionHeader.cs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
namespace LBPUnion.ProjectLighthouse.Types.Tickets;
|
||||||
|
|
||||||
|
public struct SectionHeader
|
||||||
|
{
|
||||||
|
public SectionType Type;
|
||||||
|
public ushort Length;
|
||||||
|
}
|
7
ProjectLighthouse/Types/Tickets/SectionType.cs
Normal file
7
ProjectLighthouse/Types/Tickets/SectionType.cs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
namespace LBPUnion.ProjectLighthouse.Types.Tickets;
|
||||||
|
|
||||||
|
public enum SectionType : byte
|
||||||
|
{
|
||||||
|
Body = 0x00,
|
||||||
|
Footer = 0x02,
|
||||||
|
}
|
61
ProjectLighthouse/Types/Tickets/TicketReader.cs
Normal file
61
ProjectLighthouse/Types/Tickets/TicketReader.cs
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using LBPUnion.ProjectLighthouse.Helpers.Extensions;
|
||||||
|
|
||||||
|
namespace LBPUnion.ProjectLighthouse.Types.Tickets;
|
||||||
|
|
||||||
|
public class TicketReader : BinaryReader
|
||||||
|
{
|
||||||
|
public TicketReader([NotNull] Stream input) : base(input)
|
||||||
|
{}
|
||||||
|
|
||||||
|
public Version ReadTicketVersion() => new(this.ReadByte() >> 4, this.ReadByte());
|
||||||
|
|
||||||
|
public SectionHeader ReadSectionHeader()
|
||||||
|
{
|
||||||
|
this.ReadByte();
|
||||||
|
|
||||||
|
SectionHeader sectionHeader = new();
|
||||||
|
sectionHeader.Type = (SectionType)this.ReadByte();
|
||||||
|
sectionHeader.Length = this.ReadUInt16BE();
|
||||||
|
|
||||||
|
return sectionHeader;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DataHeader ReadDataHeader()
|
||||||
|
{
|
||||||
|
DataHeader dataHeader = new();
|
||||||
|
dataHeader.Type = (DataType)this.ReadUInt16BE();
|
||||||
|
dataHeader.Length = this.ReadUInt16BE();
|
||||||
|
|
||||||
|
return dataHeader;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] ReadTicketBinary()
|
||||||
|
{
|
||||||
|
DataHeader dataHeader = this.ReadDataHeader();
|
||||||
|
Debug.Assert(dataHeader.Type == DataType.Binary || dataHeader.Type == DataType.String);
|
||||||
|
|
||||||
|
return this.ReadBytes(dataHeader.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ReadTicketString() => Encoding.UTF8.GetString(this.ReadTicketBinary()).TrimEnd('\0');
|
||||||
|
|
||||||
|
public uint ReadTicketUInt32()
|
||||||
|
{
|
||||||
|
DataHeader dataHeader = this.ReadDataHeader();
|
||||||
|
Debug.Assert(dataHeader.Type == DataType.UInt32);
|
||||||
|
|
||||||
|
return this.ReadUInt32BE();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ulong ReadTicketUInt64()
|
||||||
|
{
|
||||||
|
DataHeader dataHeader = this.ReadDataHeader();
|
||||||
|
Debug.Assert(dataHeader.Type == DataType.UInt64 || dataHeader.Type == DataType.Timestamp);
|
||||||
|
|
||||||
|
return this.ReadUInt64BE();
|
||||||
|
}
|
||||||
|
}
|
17
ProjectLighthouse/Types/Version.cs
Normal file
17
ProjectLighthouse/Types/Version.cs
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
namespace LBPUnion.ProjectLighthouse.Types;
|
||||||
|
|
||||||
|
public class Version
|
||||||
|
{
|
||||||
|
public int Major { get; set; }
|
||||||
|
public int Minor { get; set; }
|
||||||
|
|
||||||
|
public Version(int major, int minor)
|
||||||
|
{
|
||||||
|
this.Major = major;
|
||||||
|
this.Minor = minor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() => $"{this.Major}.{this.Minor}";
|
||||||
|
|
||||||
|
public static implicit operator string(Version v) => v.ToString();
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue