From f8b1f781e993c83fd4aac0448f490991c61da371 Mon Sep 17 00:00:00 2001 From: JeanxPereira Date: Thu, 19 Mar 2026 14:50:38 -0300 Subject: [PATCH 1/6] Implement login fixes --- .gitmodules | 7 +- ReCap.Server/Adapters/Blaze/BlazeServer.cs | 7 +- ReCap.Server/Adapters/Blaze/Client.cs | 2 + .../Component/AuthenticationComponent.cs | 275 ++++++++++++++---- .../GameManager/GameManagerComponent.cs | 1 + .../Blaze/Component/PlaygroupsComponent.cs | 5 + .../Blaze/Component/UserSessionsComponent.cs | 99 ++++++- ReCap.Server/Adapters/Blaze/TdfDecoder.cs | 10 +- ReCap.Server/Adapters/Blaze/TdfVector.cs | 8 + .../SQLite/AccountRepositoryAdapter.cs | 2 +- ReCap.Server/Adapters/Rest/Api.cs | 2 +- ReCap.Server/Program.cs | 2 +- ReCap.Server/Services/AccountService.cs | 8 + lib/RakNexus | 1 + lib/SharpRakNet | 1 - 15 files changed, 353 insertions(+), 77 deletions(-) create mode 160000 lib/RakNexus delete mode 160000 lib/SharpRakNet diff --git a/.gitmodules b/.gitmodules index 3d2f372..1f4fef8 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,3 @@ -[submodule "lib/SharpRakNet"] - path = lib/SharpRakNet - url = https://github.com/Resurrection-Capsule/SharpRakNet.git - branch = net8.0 +[submodule "lib/RakNexus"] + path = lib/RakNexus + url = https://github.com/JeanxPereira/RakNexus.git diff --git a/ReCap.Server/Adapters/Blaze/BlazeServer.cs b/ReCap.Server/Adapters/Blaze/BlazeServer.cs index 2008c46..deee191 100644 --- a/ReCap.Server/Adapters/Blaze/BlazeServer.cs +++ b/ReCap.Server/Adapters/Blaze/BlazeServer.cs @@ -60,7 +60,7 @@ public BlazeServer(SqliteConfig newSqliteConfig, string name, IPAddress hostAddr new MessagingComponent(), new PlaygroupsComponent(), new RoomsComponent(), - new UserSessionsComponent(), + new UserSessionsComponent(newSqliteConfig), new UtilComponent(), new GameReportingComponent(), new UnknownComponent1() @@ -130,6 +130,11 @@ public void HandlePacket(Client client, Packet packet) Log($"Unknown component: 0x{packet.Component:X}"); } + public Client? FindClientByUserId(ulong userId) + { + return Clients.FirstOrDefault(c => c.UserId == userId); + } + public void Disconnect(Client client) { Clients.Remove(client); diff --git a/ReCap.Server/Adapters/Blaze/Client.cs b/ReCap.Server/Adapters/Blaze/Client.cs index af72aaf..17e37c3 100644 --- a/ReCap.Server/Adapters/Blaze/Client.cs +++ b/ReCap.Server/Adapters/Blaze/Client.cs @@ -3,6 +3,7 @@ using System.Net; using System.Net.Sockets; using Org.BouncyCastle.Tls; +using ReCap.Server.Adapters.Blaze.Component; using ReCap.Server.Adapters.Blaze.Ssl; namespace ReCap.Server.Adapters.Blaze; @@ -23,6 +24,7 @@ public class Client public ulong UserId { get; set; } public string AuthToken { get; set; } + public UserSessionExtendedData ExtendedData { get; } = new(); public Client(BlazeServer server, TcpClient tcpClient) { diff --git a/ReCap.Server/Adapters/Blaze/Component/AuthenticationComponent.cs b/ReCap.Server/Adapters/Blaze/Component/AuthenticationComponent.cs index a133011..5531ec0 100644 --- a/ReCap.Server/Adapters/Blaze/Component/AuthenticationComponent.cs +++ b/ReCap.Server/Adapters/Blaze/Component/AuthenticationComponent.cs @@ -1,6 +1,7 @@ using System.Buffers.Binary; using System.Text; using ReCap.Server.Config; +using ReCap.Server.Models; using ReCap.Server.Services; using ReCap.Server.Util; @@ -29,9 +30,24 @@ public bool HandlePacket(Client client, Packet packet) case 0x28: return HandleLogin(client, packet); + case 0x29: + return HandleAcceptTOS(client, packet); + + case 0x2A: + return HandleGetTOSInfo(client, packet); + + case 0x2E: + return HandleGetTermsAndConditions(client, packet); + case 0x2F: return HandleGetPrivacyPolicyContent(client, packet); + case 0x32: + return HandleSilentLogin(client, packet); + + case 0x3C: + return HandleExpressLogin(client, packet); + case 0x46: return HandleLogout(client, packet); @@ -39,7 +55,7 @@ public bool HandlePacket(Client client, Packet packet) return HandleLoginPersona(client, packet); case 0xF1: - return HandleAcceptLegalDocs(client, packet); + return HandleAcceptTOS(client, packet); case 0xF2: return HandleGetEmailOptInSettings(client, packet); @@ -55,26 +71,17 @@ public bool HandlePacket(Client client, Packet packet) private bool GetAuthToken(Client client, Packet packet) { - bool generateAuthToken = client.AuthToken == null; - client.AuthToken = generateAuthToken ? Guid.NewGuid().ToString() : client.AuthToken; + client.AuthToken ??= client.UserId.ToString(); - var response = new GetAuthTokenResponse - { - AuthToken = client.AuthToken - }; - - client.RespondTo(packet, response); + accountService.setAccountAuthToken(client.UserId, client.AuthToken); - if (generateAuthToken) - { - accountService.setAccountAuthToken(client.UserId, client.AuthToken); + client.RespondTo(packet, new GetAuthTokenResponse { AuthToken = client.AuthToken }); - client.Notify(new UserStatus() - { - BlazeId = client.UserId, - StatusFlags = 3 - }, 0x7802, 5); - } + client.Notify(new UserStatus + { + BlazeId = client.UserId, + StatusFlags = (uint)SessionState.Authenticated + }, 0x7802, 5); return true; } @@ -88,12 +95,26 @@ private bool HandleLogin(Client client, Packet packet) var request = packet.ReadContent(); if (request is null) { - client.RespondTo(packet, null, error: 0x5E0001); // AUTH_ERR_NO_SUCH_AUTH_DATA + client.RespondTo(packet, null, error: 0x5E0001); + return true; + } + + Logger.info($"[Auth] Login attempt: Email='{request.Email}', Pass='{request.Password}'"); + + AccountModel account; + try + { + account = accountService.getAccountByEmailAndPassword(request.Email, request.Password); + } + catch (ForbiddenOperationException ex) + { + Logger.error($"[Auth] Login failed: {ex.Message}"); + client.RespondTo(packet, null, error: 0xB0001); return true; } - var account = accountService.getAccountByEmailAndPassword(request.Email, request.Password); client.UserId = account.Id; + InitializeClientExtendedData(client); var response = new LoginResponse { @@ -127,35 +148,28 @@ private bool HandleLoginPersona(Client client, Packet packet) var request = packet.ReadContent(); if (request is null) { - client.RespondTo(packet, null, error: 0x5E0001); // AUTH_ERR_NO_SUCH_AUTH_DATA + client.RespondTo(packet, null, error: 0x5E0001); return true; } - var account = accountService.getAccountById(client.UserId); - - var response = new SessionInfo + AccountModel account; + try { - LastLoginDateTime = CurrentUnixTime, - Email = account.Email, - UserId = account.Id, - BlazeUserId = account.Id - }; - - response.PersonaDetails.DisplayName = account.Username; - response.PersonaDetails.LastLoginTime = CurrentUnixTime; - response.PersonaDetails.PersonaId = client.UserId; - response.PersonaDetails.Status = PersonaStatus.Active; + account = accountService.getAccountById(client.UserId); + } + catch + { + client.RespondTo(packet, null, error: 0xB0001); + return true; + } - client.RespondTo(packet, response); + client.RespondTo(packet, BuildSessionInfo(account, client)); var addrBytes = client.EndPoint.Address.GetAddressBytes(); - var addr = addrBytes.Length == 4 ? BinaryPrimitives.ReadUInt32BigEndian(addrBytes) : 0; var userAdded = new NotifyUserAdded(); - userAdded.ExtendedData.Address.ActiveMember = NetworkAddressMember.IpPairAddress; - userAdded.ExtendedData.Address.IpPairAddress.ExternalAddress.Ip = addr; - userAdded.ExtendedData.Address.IpPairAddress.ExternalAddress.Port = (ushort)client.EndPoint.Port; + CopyExtendedDataToNotification(client, userAdded.ExtendedData, addr); userAdded.UserInfo.AccountId = client.UserId; userAdded.UserInfo.AccountLocale = 0x656E5553; userAdded.UserInfo.BlazeId = client.UserId; @@ -163,32 +177,12 @@ private bool HandleLoginPersona(Client client, Packet packet) client.Notify(userAdded, 0x7802, 2); - client.Notify(new UserStatus() + client.Notify(new UserStatus { BlazeId = account.Id, - StatusFlags = 2 + StatusFlags = (uint)SessionState.Connected }, 0x7802, 5); - var update = new UserSessionExtendedDataUpdate(); - update.ExtendedData.Address.ActiveMember = NetworkAddressMember.IpPairAddress; - update.ExtendedData.Address.IpPairAddress.ExternalAddress.Ip = addr; - update.ExtendedData.Address.IpPairAddress.ExternalAddress.Port = (ushort)client.EndPoint.Port; - update.ExtendedData.UserInfoAttribute = 0x4000000000000000; // disable popup about multiple locations - - client.Notify(update, 0x7802, 1); - - client.Notify(new UserSessionLoginInfo - { - AccountLocale = 0x656E5553, - BlazeUserId = client.UserId, - DisplayName = account.Username, - LastLoginTime = CurrentUnixTime, - LastLoginDateTime = CurrentUnixTime, - Email = account.Email, - PersonaId = client.UserId, - Platform = ConnectionProfileType.PC, - UserId = client.UserId - }, 0x7802, 8); return true; } @@ -220,12 +214,141 @@ private static bool HandleGetLegalDocContent(Client client, Packet packet) private static bool HandleGetPrivacyPolicyContent(Client client, Packet packet) { - return false; + client.RespondTo(packet, new GetLegalDocContentResponse()); + return true; + } + + private static bool HandleAcceptTOS(Client client, Packet packet) + { + client.RespondTo(packet); + return true; + } + + private static bool HandleGetTOSInfo(Client client, Packet packet) + { + client.RespondTo(packet, new GetEmailOptInSettingsResponse()); + return true; + } + + private static bool HandleGetTermsAndConditions(Client client, Packet packet) + { + client.RespondTo(packet, new GetLegalDocContentResponse + { + LDVC = "Something", + Length = 23, + Text = "Hello this is something" + }); + return true; + } + + private static void CopyExtendedDataToNotification(Client client, UserSessionExtendedData target, uint addr) + { + target.Address.ActiveMember = NetworkAddressMember.IpPairAddress; + target.Address.IpPairAddress.ExternalAddress.Ip = addr; + target.Address.IpPairAddress.ExternalAddress.Port = (ushort)client.EndPoint.Port; + target.Country = client.ExtendedData.Country; + target.HardwareFlags = client.ExtendedData.HardwareFlags; + target.UserInfoAttribute = client.ExtendedData.UserInfoAttribute; + foreach (var obj in client.ExtendedData.BlazeObjectIdList) + target.BlazeObjectIdList.Add(obj); + foreach (var latency in client.ExtendedData.LatencyList) + target.LatencyList.Add(latency); + target.QosData.DownstreamBitsPerSecond = client.ExtendedData.QosData.DownstreamBitsPerSecond; + target.QosData.NatType = client.ExtendedData.QosData.NatType; + target.QosData.UpstreamBitsPerSecond = client.ExtendedData.QosData.UpstreamBitsPerSecond; + } + + private bool HandleSilentLogin(Client client, Packet packet) + { + var request = packet.ReadContent(); + if (request is null) + { + client.RespondTo(packet, null, error: 0x5E0001); + return true; + } + + AccountModel account; + try + { + account = accountService.getAccountByAuthToken(request.AuthToken); + } + catch + { + client.RespondTo(packet, null, error: 0xB0001); + return true; + } + + client.UserId = account.Id; + client.AuthToken = request.AuthToken; + InitializeClientExtendedData(client); + + client.RespondTo(packet, BuildSessionInfo(account, client)); + return true; + } + + private bool HandleExpressLogin(Client client, Packet packet) + { + var request = packet.ReadContent(); + if (request is null) + { + client.RespondTo(packet, null, error: 0x5E0001); + return true; + } + + AccountModel account; + try + { + account = accountService.getAccountByEmailAndPassword(request.Email, request.Password); + } + catch + { + client.RespondTo(packet, null, error: 0xB0001); + return true; + } + + client.UserId = account.Id; + InitializeClientExtendedData(client); + + client.RespondTo(packet, BuildSessionInfo(account, client)); + return true; + } + + private static void InitializeClientExtendedData(Client client) + { + if (client.ExtendedData.BlazeObjectIdList.Count > 0) + return; + + client.ExtendedData.Country = "US"; + client.ExtendedData.HardwareFlags = 1; + client.ExtendedData.UserInfoAttribute = 3; + + client.ExtendedData.BlazeObjectIdList.Add(new BlazeObjectId(0, new BlazeObjectType(4, 1))); + client.ExtendedData.BlazeObjectIdList.Add(new BlazeObjectId(0, new BlazeObjectType(5, 1))); + + for (int i = 0; i < 5; i++) + client.ExtendedData.LatencyList.Add(1161889797); + + client.ExtendedData.QosData.DownstreamBitsPerSecond = 128000; + client.ExtendedData.QosData.NatType = NatType.Open; + client.ExtendedData.QosData.UpstreamBitsPerSecond = 2; } - private static bool HandleAcceptLegalDocs(Client client, Packet packet) + private static SessionInfo BuildSessionInfo(AccountModel account, Client client) { - return false; + var info = new SessionInfo + { + LastLoginDateTime = CurrentUnixTime, + Email = account.Email, + UserId = account.Id, + BlazeUserId = account.Id + }; + + info.PersonaDetails.DisplayName = account.Username; + info.PersonaDetails.LastLoginTime = CurrentUnixTime; + info.PersonaDetails.PersonaId = client.UserId; + info.PersonaDetails.Status = PersonaStatus.Active; + + return info; } public string GetCommandName(ushort id) @@ -490,3 +613,27 @@ public class SessionInfo : Tdf [TdfField("UID", 0)] public ulong UserId { get; set; } } + +public class SilentLoginRequest : Tdf +{ + [TdfField("AUTH", "")] + public string AuthToken { get; set; } = string.Empty; + + [TdfField("PID", 0)] + public long PersonaId { get; set; } + + [TdfField("TYPE", AuthenticationTokenType.Unknown)] + public AuthenticationTokenType TokenType { get; set; } = AuthenticationTokenType.Unknown; +} + +public class ExpressLoginRequest : Tdf +{ + [TdfField("MAIL", "")] + public string Email { get; set; } = string.Empty; + + [TdfField("PASS", "")] + public string Password { get; set; } = string.Empty; + + [TdfField("PNAM", "")] + public string PersonaName { get; set; } = string.Empty; +} diff --git a/ReCap.Server/Adapters/Blaze/Component/GameManager/GameManagerComponent.cs b/ReCap.Server/Adapters/Blaze/Component/GameManager/GameManagerComponent.cs index dde7486..2c68aaf 100644 --- a/ReCap.Server/Adapters/Blaze/Component/GameManager/GameManagerComponent.cs +++ b/ReCap.Server/Adapters/Blaze/Component/GameManager/GameManagerComponent.cs @@ -188,6 +188,7 @@ private bool HandleUpdateMeshConnection(Client client, Packet packet) Log($"UpdateMeshConnection: {request}"); + client.RespondTo(packet); return true; } diff --git a/ReCap.Server/Adapters/Blaze/Component/PlaygroupsComponent.cs b/ReCap.Server/Adapters/Blaze/Component/PlaygroupsComponent.cs index dda7940..23b2e5b 100644 --- a/ReCap.Server/Adapters/Blaze/Component/PlaygroupsComponent.cs +++ b/ReCap.Server/Adapters/Blaze/Component/PlaygroupsComponent.cs @@ -24,6 +24,11 @@ public bool HandlePacket(Client client, Packet packet) private bool HandleCreatePlaygroupPacket(Client client, Packet packet) { var request = packet.ReadContent(); + if (request is null) + { + client.RespondTo(packet, null, error: 0x5E0001); + return true; + } client.RespondTo(packet, new JoinPlaygroupResponse() { Info = request.Info }); return true; diff --git a/ReCap.Server/Adapters/Blaze/Component/UserSessionsComponent.cs b/ReCap.Server/Adapters/Blaze/Component/UserSessionsComponent.cs index 84227d2..ada2fec 100644 --- a/ReCap.Server/Adapters/Blaze/Component/UserSessionsComponent.cs +++ b/ReCap.Server/Adapters/Blaze/Component/UserSessionsComponent.cs @@ -1,13 +1,23 @@ using System.Buffers.Binary; +using ReCap.Server.Config; +using ReCap.Server.Models; +using ReCap.Server.Services; using ReCap.Server.Util; namespace ReCap.Server.Adapters.Blaze.Component; public class UserSessionsComponent : IComponent { + private AccountService accountService; + public ushort Id { get; } = 0x7802; public BlazeServer? Server { get; set; } + public UserSessionsComponent(SqliteConfig sqliteConfig) + { + accountService = new AccountService(sqliteConfig); + } + public bool HandlePacket(Client client, Packet packet) { switch (packet.Command) @@ -15,6 +25,9 @@ public bool HandlePacket(Client client, Packet packet) case 0x05: return HandleUpdateExtendedDataAttribute(client, packet); + case 0x0C: + return HandleLookupUser(client, packet); + case 0x14: return HandleUpdateNetworkInfo(client, packet); @@ -80,7 +93,9 @@ private static bool HandleUpdateNetworkInfo(Client client, Packet packet) private static bool HandleUpdateUserSessionClientData(Client client, Packet packet) { - client.RespondTo(packet); + var response = new UpdateUserSessionClientDataResponse(); + response.ClientVariables.Add(1); + client.RespondTo(packet, response); return true; } @@ -95,7 +110,54 @@ private static bool HandleSetUserInfoAttribute(Client client, Packet packet) Log($"SetUserInfoAttribute: {request} (0x{request.AttributeBits:X}, 0x{request.MaskBits:X})"); - //client.RespondTo(packet); + client.RespondTo(packet); + return true; + } + + private bool HandleLookupUser(Client client, Packet packet) + { + var request = packet.ReadContent(); + if (request is null) + { + Log("Unable to read content of lookupUser request!"); + return false; + } + + var target = Server?.FindClientByUserId(request.UserId); + if (target is null) + { + client.RespondTo(packet, null, error: 0x5E0001); + return true; + } + + AccountModel account; + try + { + account = accountService.getAccountById(target.UserId); + } + catch + { + client.RespondTo(packet, null, error: 0xB0001); + return true; + } + + var response = new LookupUserResponse(); + response.ExtendedData.Country = target.ExtendedData.Country; + response.ExtendedData.HardwareFlags = target.ExtendedData.HardwareFlags; + response.ExtendedData.UserInfoAttribute = target.ExtendedData.UserInfoAttribute; + foreach (var obj in target.ExtendedData.BlazeObjectIdList) + response.ExtendedData.BlazeObjectIdList.Add(obj); + foreach (var latency in target.ExtendedData.LatencyList) + response.ExtendedData.LatencyList.Add(latency); + response.ExtendedData.QosData.DownstreamBitsPerSecond = target.ExtendedData.QosData.DownstreamBitsPerSecond; + response.ExtendedData.QosData.NatType = target.ExtendedData.QosData.NatType; + response.ExtendedData.QosData.UpstreamBitsPerSecond = target.ExtendedData.QosData.UpstreamBitsPerSecond; + response.StatusFlags = (uint)SessionState.Authenticated; + response.UserInfo.AccountId = account.Id; + response.UserInfo.BlazeId = account.Id; + response.UserInfo.Name = account.Username; + + client.RespondTo(packet, response); return true; } @@ -319,4 +381,37 @@ public class SetUserInfoAttributeRequest : Tdf [TdfField("ULST")] public TdfPrimitiveVector BlazeObjectIdList { get; } = []; +} + +public enum SessionState +{ + Idle = 0, + Connecting = 1, + Connected = 2, + Authenticated = 3, + Invalid = 4 +} + +public class LookupUserRequest : Tdf +{ + [TdfField("UID", 0)] + public ulong UserId { get; set; } +} + +public class LookupUserResponse : Tdf +{ + [TdfField("DATA")] + public UserSessionExtendedData ExtendedData { get; } = new(); + + [TdfField("FLGS", 0)] + public uint StatusFlags { get; set; } + + [TdfField("USER")] + public UserIdentification UserInfo { get; } = new(); +} + +public class UpdateUserSessionClientDataResponse : Tdf +{ + [TdfField("CVAR")] + public TdfPrimitiveVector ClientVariables { get; } = []; } \ No newline at end of file diff --git a/ReCap.Server/Adapters/Blaze/TdfDecoder.cs b/ReCap.Server/Adapters/Blaze/TdfDecoder.cs index 346a89a..4c2b6ce 100644 --- a/ReCap.Server/Adapters/Blaze/TdfDecoder.cs +++ b/ReCap.Server/Adapters/Blaze/TdfDecoder.cs @@ -1,6 +1,7 @@ using System.Diagnostics; using System.Runtime.InteropServices; using System.Text; +using ReCap.Server.Util; namespace ReCap.Server.Adapters.Blaze; @@ -494,7 +495,9 @@ private void SkipElement(TdfType type) break; default: - throw new Exception($"Unable to skip unknown type: {type}"); + Logger.info($"TdfDecoder: Skipping unknown type {type}, consuming remaining struct"); + ConsumeStructTerminator(); + break; } } @@ -506,7 +509,10 @@ private bool ValidateHeader(uint tag, TdfType type) var b3 = (TdfType)Reader.ReadByte(); if (b3 > TdfType.TimeValue) - throw new Exception($"Invalid type ({b3}) found in GetHeader!"); + { + Logger.info($"TdfDecoder: Invalid type ({b3}) in header, skipping"); + return false; + } var foundTag = b0 | b1 | b2; if (foundTag != tag) diff --git a/ReCap.Server/Adapters/Blaze/TdfVector.cs b/ReCap.Server/Adapters/Blaze/TdfVector.cs index 9b2ce1c..5acef54 100644 --- a/ReCap.Server/Adapters/Blaze/TdfVector.cs +++ b/ReCap.Server/Adapters/Blaze/TdfVector.cs @@ -164,6 +164,14 @@ public override void EncodeMembers(TdfEncoder encoder) encoder.EncodeBinary("", (TdfBlob)objElem); break; + case "BlazeObjectId": + encoder.EncodeBlazeObjectId("", (BlazeObjectId)objElem); + break; + + case "BlazeObjectType": + encoder.EncodeBlazeObjectType("", (BlazeObjectType)objElem); + break; + default: throw new Exception($"Unknown type ({typeof(T).FullName}) in PrimitiveVector!"); } diff --git a/ReCap.Server/Adapters/Persistence/SQLite/AccountRepositoryAdapter.cs b/ReCap.Server/Adapters/Persistence/SQLite/AccountRepositoryAdapter.cs index 1708011..e674a43 100644 --- a/ReCap.Server/Adapters/Persistence/SQLite/AccountRepositoryAdapter.cs +++ b/ReCap.Server/Adapters/Persistence/SQLite/AccountRepositoryAdapter.cs @@ -57,7 +57,7 @@ public AccountModel getAccountById(ulong id) public AccountModel getAccountByEmail(string email) { - return sqliteConfig.Accounts.SingleOrDefault(b => b.Email == email); + return sqliteConfig.Accounts.SingleOrDefault(b => b.Email.ToLower() == email.ToLower()); } public AccountModel insertAccount(Account account) diff --git a/ReCap.Server/Adapters/Rest/Api.cs b/ReCap.Server/Adapters/Rest/Api.cs index 372936d..bf26201 100644 --- a/ReCap.Server/Adapters/Rest/Api.cs +++ b/ReCap.Server/Adapters/Rest/Api.cs @@ -13,7 +13,7 @@ namespace ReCap.Server.Adapters.Rest.Api; public class Api { - public const int DEFAULT_PORT = 80; + public const int DEFAULT_PORT = 8033; private List restControllers; readonly int _port; diff --git a/ReCap.Server/Program.cs b/ReCap.Server/Program.cs index 6cd3658..fcd2c71 100644 --- a/ReCap.Server/Program.cs +++ b/ReCap.Server/Program.cs @@ -125,7 +125,7 @@ static async Task Main(string[] args) Task afterRelaunch = new(() => { - raknet.Listener.StopListener(); + raknet.Listener.Stop(); lobby.Stop(); redirector.Stop(); restClientAdapter.Stop(); diff --git a/ReCap.Server/Services/AccountService.cs b/ReCap.Server/Services/AccountService.cs index 4ee58a6..9c670f9 100644 --- a/ReCap.Server/Services/AccountService.cs +++ b/ReCap.Server/Services/AccountService.cs @@ -32,6 +32,14 @@ public AccountModel getAccountByEmailAndPassword(string email, string password) return account; } + public AccountModel getAccountByEmail(string email) { + var account = accountRepository.getAccountByEmail(email); + if (account == null) { + throw new ForbiddenOperationException("This e-mail does not belong to any account"); + } + return account; + } + public void deleteAuthToken(string authToken) { accountRepository.deleteAuthToken(authToken); } diff --git a/lib/RakNexus b/lib/RakNexus new file mode 160000 index 0000000..9d63ccf --- /dev/null +++ b/lib/RakNexus @@ -0,0 +1 @@ +Subproject commit 9d63ccf91c88c755f9d5122774cdda16d0813fa2 diff --git a/lib/SharpRakNet b/lib/SharpRakNet deleted file mode 160000 index 1ca34c4..0000000 --- a/lib/SharpRakNet +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 1ca34c40f8c340d4775c90c5d65e97fcd17918dc From 263cdeae0443c420befc88a9e661d74c114644a6 Mon Sep 17 00:00:00 2001 From: JeanxPereira Date: Thu, 19 Mar 2026 14:54:06 -0300 Subject: [PATCH 2/6] Replace SharpRakNet with RakNexus in RakNet adapters --- ReCap.Server/Adapters/RakNet/RakNetClient.cs | 10 +- ReCap.Server/Adapters/RakNet/RakNetServer.cs | 108 ++++++++----------- ReCap.Server/ReCap.Server.csproj | 2 +- 3 files changed, 48 insertions(+), 72 deletions(-) diff --git a/ReCap.Server/Adapters/RakNet/RakNetClient.cs b/ReCap.Server/Adapters/RakNet/RakNetClient.cs index 382eccb..bd664f6 100644 --- a/ReCap.Server/Adapters/RakNet/RakNetClient.cs +++ b/ReCap.Server/Adapters/RakNet/RakNetClient.cs @@ -1,15 +1,15 @@ -using SharpRakNet.Network; -using SharpRakNet.Protocol.Raknet; +using RakNexus.Network; +using RakNexus.Protocol; using ReCap.Server.Domain.Gameplay; using ReCap.Server.Adapters.RakNet.Packets; namespace ReCap.Server.Adapters.RakNet; -public record RakNetClient(RaknetSession Session) +public record RakNetClient(RakNetSession Session) { public ulong UserId { get; set; } public ulong PlaygroupId { get; set; } public Game? Game { get; set; } - public void SendPacket(IRakNetPacket packet, Reliability reliability = Reliability.ReliableOrdered) => RakNetServer.SendPacket(this, packet, reliability); -} \ No newline at end of file + public void SendPacket(IRakNetPacket packet, PacketReliability reliability = PacketReliability.RELIABLE_ORDERED) => RakNetServer.SendPacket(this, packet, reliability); +} diff --git a/ReCap.Server/Adapters/RakNet/RakNetServer.cs b/ReCap.Server/Adapters/RakNet/RakNetServer.cs index b16e18a..e2ccf9e 100644 --- a/ReCap.Server/Adapters/RakNet/RakNetServer.cs +++ b/ReCap.Server/Adapters/RakNet/RakNetServer.cs @@ -1,8 +1,6 @@ -using System.Net; -// using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using SharpRakNet.Network; -using SharpRakNet.Protocol.Raknet; +using System.Net; +using RakNexus.Network; +using RakNexus.Protocol; using ReCap.Server.Domain.Gameplay; using ReCap.Server.Adapters.RakNet.Packets; using ReCap.Server.Adapters.Blaze.Component.GameManager; @@ -17,11 +15,11 @@ public class RakNetServer private AccountService accountService; private GameService gameService; - public Dictionary Clients { get; } = new(); // RakNet Guid -> Client - public Dictionary Games { get; } = new(); // GameId -> Game - public Dictionary GameAssigments { get; } = new(); // UserId -> GameId + public Dictionary Clients { get; } = new(); + public Dictionary Games { get; } = new(); + public Dictionary GameAssigments { get; } = new(); - public RaknetListener Listener { get; } + public RakNetListener Listener { get; } public bool IsRunning { get; private set; } public ulong GameCounter { get; private set; } = 0x0080000000000001; @@ -30,70 +28,55 @@ public RakNetServer(SqliteConfig newSqliteConfig, string name, IPAddress hostAdd accountService = new AccountService(newSqliteConfig); gameService = new GameService(); - Listener = new RaknetListener(new IPEndPoint(hostAddress, port)) - { - SessionConnected = OnSessionConnected, - SessionDisconnected = OnSessionDisconnected - }; + Listener = new RakNetListener(port); + Listener.SessionConnected += OnSessionConnected; } - private void OnSessionConnected(RaknetSession session) + private void OnSessionConnected(RakNetSession session) { - Logger.info($"RakNet: Peer 0x{session.Guid} connected {session.PeerEndPoint}!"); + Logger.info($"RakNet: Peer 0x{session.Guid.G:X16} connected {session.Address}!"); - if (Clients.ContainsKey(session.Guid)) + if (Clients.ContainsKey(session.Guid.G)) { - Logger.error($"RakNet: Peer 0x{session.Guid} already has an assigned client!"); + Logger.error($"RakNet: Peer 0x{session.Guid.G:X16} already has an assigned client!"); return; } - session.SessionReceiveRaw += OnSessionReceiveRaw; - session.SessionOnNewIncomingConnection += OnSessionOnNewIncomingConnection; + session.PacketReceived += packet => OnSessionReceiveRaw(session, packet); + session.OnNewIncomingConnection += () => OnSessionOnNewIncomingConnection(session); + session.Disconnected += reason => OnSessionDisconnected(session); - Clients.Add(session.Guid, new RakNetClient(session)); + Clients.Add(session.Guid.G, new RakNetClient(session)); } - private void OnSessionDisconnected(RaknetSession session) + private void OnSessionDisconnected(RakNetSession session) { - Logger.info($"RakNet: Peer 0x{session.Guid} disconnected {session.PeerEndPoint}!"); - - if (Clients.TryGetValue(session.Guid, out var client)) - { - // TODO: signal it to the game? + Logger.info($"RakNet: Peer 0x{session.Guid.G:X16} disconnected {session.Address}!"); - session.SessionReceiveRaw -= OnSessionReceiveRaw; - session.SessionOnNewIncomingConnection -= OnSessionOnNewIncomingConnection; - - Clients.Remove(session.Guid); - } - else - Logger.error($"RakNet: Peer 0x{session.Guid} had no assigned client!"); + if (!Clients.Remove(session.Guid.G)) + Logger.error($"RakNet: Peer 0x{session.Guid.G:X16} had no assigned client!"); } - private void OnSessionOnNewIncomingConnection(RaknetSession session) => SendPacket(session, new ConnectedPacket()); + private void OnSessionOnNewIncomingConnection(RakNetSession session) => SendPacket(session, new ConnectedPacket()); - private bool OnSessionReceiveRaw(RaknetSession session, byte[] data) + private void OnSessionReceiveRaw(RakNetSession session, Packet rakPacket) { + var data = rakPacket.Data; var packetType = (PacketType)data[0]; - if (true) // packetType != PacketType.ClockSync - { - Logger.info($"RakNet: Receiving {packetType} packet from {session.PeerEndPoint}! Data: {BitConverter.ToString(data)}!"); - } + Logger.info($"RakNet: Receiving {packetType} packet from {session.Address}! Data: {BitConverter.ToString(data)}!"); var packet = PacketActivator.CreateInstance(data); if (packet is null) { - Logger.error($"RakNet: Peer 0x{session.Guid} has sent an unhandled packet ({packetType})! Skipping..."); - - return false; + Logger.error($"RakNet: Peer 0x{session.Guid.G:X16} has sent an unhandled packet ({packetType})! Skipping..."); + return; } - if (!Clients.TryGetValue(session.Guid, out var client)) + if (!Clients.TryGetValue(session.Guid.G, out var client)) { - Logger.error($"RakNet: No client was found for peer 0x{session.Guid}, but received a packet ({packetType})!"); - - return false; + Logger.error($"RakNet: No client was found for peer 0x{session.Guid.G:X16}, but received a packet ({packetType})!"); + return; } switch (packet) @@ -101,7 +84,7 @@ private bool OnSessionReceiveRaw(RaknetSession session, byte[] data) case HelloPlayerRequestPacket helloPlayerRequestPacket: client.UserId = helloPlayerRequestPacket.UserId; client.PlaygroupId = helloPlayerRequestPacket.PlaygroupId; - + var account = accountService.getAccountById(client.UserId); var game = gameService.GetGameByPlayer(account); if (game == null) @@ -110,35 +93,28 @@ private bool OnSessionReceiveRaw(RaknetSession session, byte[] data) gameService.AddPlayerToGame(game.Id, account); } - // Clients.Remove(session.Guid); - - // session.Disconnect(); - - return true; + return; } if (client.Game is not null) { client.Game.HandlePacket(client, packet); - return true; + return; } - Logger.error($"RakNet: Peer 0x{session.Guid} has no game, but received a packet ({packetType}) intended for a game!"); - - return false; + Logger.error($"RakNet: Peer 0x{session.Guid.G:X16} has no game, but received a packet ({packetType}) intended for a game!"); } public async Task ExecuteAsync(CancellationToken stoppingToken) { - Listener.BeginListener(); + _ = Task.Run(() => Listener.StartAsync(), stoppingToken); IsRunning = true; - Logger.info($"[RakNet]: Started listening on {Listener.Socket.Socket.Client.LocalEndPoint}!"); + Logger.info($"[RakNet]: Started listening!"); try { - // Definitely the wrong way to do this but I don't have a lot of experience with C# multithreading... while (IsRunning) { var games = gameService.GetAllGames(); @@ -153,12 +129,12 @@ public async Task ExecuteAsync(CancellationToken stoppingToken) { } - Listener.StopListener(); + Listener.Stop(); Logger.info("RakNet: Stopped listening!"); } - public void SendPacket(ulong guid, IRakNetPacket packet, Reliability reliability = Reliability.ReliableOrdered) + public void SendPacket(ulong guid, IRakNetPacket packet, PacketReliability reliability = PacketReliability.RELIABLE_ORDERED) { if (Clients.TryGetValue(guid, out var client)) SendPacket(client.Session, packet, reliability); @@ -166,10 +142,10 @@ public void SendPacket(ulong guid, IRakNetPacket packet, Reliability reliability Logger.error($"No session found for session id: {guid}, unable to send packet {packet.Type} to it!"); } - public static void SendPacket(RakNetClient client, IRakNetPacket packet, Reliability reliability = Reliability.ReliableOrdered) + public static void SendPacket(RakNetClient client, IRakNetPacket packet, PacketReliability reliability = PacketReliability.RELIABLE_ORDERED) => SendPacket(client.Session, packet, reliability); - - public static void SendPacket(RaknetSession session, IRakNetPacket packet, Reliability reliability = Reliability.ReliableOrdered) + + public static void SendPacket(RakNetSession session, IRakNetPacket packet, PacketReliability reliability = PacketReliability.RELIABLE_ORDERED) { using var ms = new MemoryStream(); @@ -177,6 +153,6 @@ public static void SendPacket(RaknetSession session, IRakNetPacket packet, Relia packet.WriteTo(ms); - session.Sendq.Insert(reliability, ms.ToArray()); + session.Send(ms.ToArray(), PacketPriority.MEDIUM_PRIORITY, reliability, 0, 0); } } diff --git a/ReCap.Server/ReCap.Server.csproj b/ReCap.Server/ReCap.Server.csproj index 1366114..0e20954 100755 --- a/ReCap.Server/ReCap.Server.csproj +++ b/ReCap.Server/ReCap.Server.csproj @@ -8,7 +8,7 @@ - + From ae5589838c1201eb106bd3ddf2ba26b5d9741350 Mon Sep 17 00:00:00 2001 From: JeanxPereira Date: Thu, 19 Mar 2026 14:56:05 -0300 Subject: [PATCH 3/6] Implement TryRerunElevated for macOS using sudo --- .../MacOSProcessPermissionsImpl.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/ReCap.Server/Util/ProcessPermissions/MacOSProcessPermissionsImpl.cs b/ReCap.Server/Util/ProcessPermissions/MacOSProcessPermissionsImpl.cs index 2eeaf9a..0b902dc 100644 --- a/ReCap.Server/Util/ProcessPermissions/MacOSProcessPermissionsImpl.cs +++ b/ReCap.Server/Util/ProcessPermissions/MacOSProcessPermissionsImpl.cs @@ -11,8 +11,18 @@ internal class MacOSProcessPermissionsImpl { public override bool TryRerunElevated(string args, out Process elevatedProcess) { - // [TODO: Implement] - throw new NotImplementedException(); + try + { + string sudoArgs = $"{CommandLineHelper.WrapArg(Environment.ProcessPath)} {args}"; + ProcessStartInfo startInfo = new("sudo", sudoArgs); + elevatedProcess = Process.Start(startInfo); + return true; + } + catch + { + elevatedProcess = default; + return false; + } } } #nullable restore \ No newline at end of file From 1d127c21fc443133320c0d6ed35a01ff0e2f272f Mon Sep 17 00:00:00 2001 From: JeanxPereira Date: Thu, 19 Mar 2026 20:52:15 -0300 Subject: [PATCH 4/6] Fix account creation --- .gitmodules | 3 + ReCap.Server/Adapters/Blaze/BlazeServer.cs | 12 +- ReCap.Server/Adapters/Blaze/Client.cs | 68 ++++++++--- .../GameManager/GameManagerComponent.cs | 114 ++++++++++-------- ReCap.Server/Config/ServerConfig.cs | 5 + ReCap.Server/Config/ServerConfigOptions.cs | 8 ++ ReCap.Server/Models/AccountModel.cs | 63 +++++----- ReCap.Server/Services/AccountService.cs | 6 +- ReCap.Server/Services/AssetDatabase.cs | 41 +++++++ ReCap.Server/Services/GameService.cs | 44 +++++-- lib/AssetData.Parser | 1 + 11 files changed, 250 insertions(+), 115 deletions(-) create mode 100644 ReCap.Server/Services/AssetDatabase.cs create mode 160000 lib/AssetData.Parser diff --git a/.gitmodules b/.gitmodules index 1f4fef8..d15066c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "lib/RakNexus"] path = lib/RakNexus url = https://github.com/JeanxPereira/RakNexus.git +[submodule "lib/AssetData.Parser"] + path = lib/AssetData.Parser + url = https://github.com/JeanxPereira/AssetData.Parser.git diff --git a/ReCap.Server/Adapters/Blaze/BlazeServer.cs b/ReCap.Server/Adapters/Blaze/BlazeServer.cs index deee191..d12b265 100644 --- a/ReCap.Server/Adapters/Blaze/BlazeServer.cs +++ b/ReCap.Server/Adapters/Blaze/BlazeServer.cs @@ -1,4 +1,4 @@ -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; using System.Net; using System.Net.Sockets; using Org.BouncyCastle.Crypto; @@ -7,6 +7,7 @@ using ReCap.Server.Adapters.Blaze.Component; using ReCap.Server.Adapters.Blaze.Component.GameManager; using ReCap.Server.Config; +using ReCap.Server.Services; namespace ReCap.Server.Adapters.Blaze; @@ -28,7 +29,7 @@ public class BlazeServer public int Port { get; } public bool Running { get; private set; } - public BlazeServer(SqliteConfig newSqliteConfig, string name, IPAddress hostAddress, int port, bool isSecure, string hostname) + public BlazeServer(SqliteConfig newSqliteConfig, string name, IPAddress hostAddress, int port, bool isSecure, string hostname, GameService? sharedGameService = null) { Name = name; IsSecure = isSecure; @@ -53,10 +54,14 @@ public BlazeServer(SqliteConfig newSqliteConfig, string name, IPAddress hostAddr }); } else { + var gameService = sharedGameService ?? new GameService(); + var gameManagerComponent = new GameManagerComponent(newSqliteConfig); + gameManagerComponent.GameHandler = gameService; + List components = new List { new AssociationListsComponent(), new AuthenticationComponent(newSqliteConfig), - new GameManagerComponent(newSqliteConfig), + gameManagerComponent, new MessagingComponent(), new PlaygroupsComponent(), new RoomsComponent(), @@ -65,7 +70,6 @@ public BlazeServer(SqliteConfig newSqliteConfig, string name, IPAddress hostAddr new GameReportingComponent(), new UnknownComponent1() }; - Dictionary Components = []; foreach (var component in components) { AttachComponent(component); } diff --git a/ReCap.Server/Adapters/Blaze/Client.cs b/ReCap.Server/Adapters/Blaze/Client.cs index 17e37c3..62f8708 100644 --- a/ReCap.Server/Adapters/Blaze/Client.cs +++ b/ReCap.Server/Adapters/Blaze/Client.cs @@ -1,4 +1,4 @@ -using System.Buffers; +using System.Buffers; using System.Diagnostics.CodeAnalysis; using System.Net; using System.Net.Sockets; @@ -10,6 +10,13 @@ namespace ReCap.Server.Adapters.Blaze; public class Client { + private static readonly Dictionary<(ushort, ushort), DateTime> _lastLogTime = new(); + private static readonly TimeSpan _logThrottle = TimeSpan.FromSeconds(30); + private static readonly HashSet<(ushort component, ushort command)> _throttledPackets = new() + { + (0x7802, 0x19), // UserSessions -> updateUserSessionClientData + }; + private byte[] ReceiveBuffer { get; } private TcpClient TcpClient { get; } @@ -74,7 +81,8 @@ public void RespondTo(Packet request, Tdf? response = null, uint error = 0) private void SendPacket(Packet packet) { - Log($"Sending {packet.ToString(Server.GetComponentAndCommandName(packet.Component, packet.Command, packet.Type == PacketType.Notification))}..."); + if (!IsThrottled(packet.Component, packet.Command)) + Log($"Sending {packet.ToString(Server.GetComponentAndCommandName(packet.Component, packet.Command, packet.Type == PacketType.Notification))}..."); packet.WriteTo(CommStream); @@ -146,36 +154,37 @@ private async void Receive() if (writeOffset <= 0) continue; - if (writeOffset < Packet.SmallestValidHeaderSize) - continue; - try { - using var ms = new MemoryStream(ReceiveBuffer, 0, writeOffset, false); + while (writeOffset >= Packet.SmallestValidHeaderSize) + { + using var ms = new MemoryStream(ReceiveBuffer, 0, writeOffset, false); - var packet = Packet.Parse(ms); - if (packet is null) - continue; + var packet = Packet.Parse(ms); + if (packet is null) + break; // Need more data for a complete packet - var totalPacketLength = (int)ms.Position; + var totalPacketLength = (int)ms.Position; - if (packet.Component != 0x2678) // If I don't ignore that specific component, the log gets spammed - { - Log($"Incoming packet: {packet.ToString(Server.GetComponentAndCommandName(packet.Component, packet.Command, packet.Type == PacketType.Notification))}"); - } + if (!IsThrottled(packet.Component, packet.Command)) + { + Log($"Incoming packet: {packet.ToString(Server.GetComponentAndCommandName(packet.Component, packet.Command, packet.Type == PacketType.Notification))}"); + } - Server.HandlePacket(this, packet); + Server.HandlePacket(this, packet); - // Shift extra read bytes to the beginning of the buffer, if any - if (writeOffset > totalPacketLength) - Array.Copy(ReceiveBuffer, 0, ReceiveBuffer, totalPacketLength, writeOffset - totalPacketLength); + // Shift extra read bytes to the beginning of the buffer, if any + if (writeOffset > totalPacketLength) + Array.Copy(ReceiveBuffer, totalPacketLength, ReceiveBuffer, 0, writeOffset - totalPacketLength); - writeOffset -= totalPacketLength; + writeOffset -= totalPacketLength; + } } catch (Exception e) { Log($"Exception while handling incoming packet! Exception: {e}"); - continue; + Disconnect(); + return; } CommStream.Flush(); @@ -184,6 +193,25 @@ private async void Receive() Disconnect(); } + private static bool IsThrottled(ushort component, ushort command) + { + if (!_throttledPackets.Contains((component, command))) + return false; + + var key = (component, command); + var now = DateTime.UtcNow; + + lock (_lastLogTime) + { + if (_lastLogTime.TryGetValue(key, out var last) && now - last < _logThrottle) + return true; + + _lastLogTime[key] = now; + } + + return false; + } + private async void Log(string message) => await Console.Out.WriteLineAsync($"[{Server.Name} Client: {EndPoint}]: {message}"); } diff --git a/ReCap.Server/Adapters/Blaze/Component/GameManager/GameManagerComponent.cs b/ReCap.Server/Adapters/Blaze/Component/GameManager/GameManagerComponent.cs index 2c68aaf..6144c79 100644 --- a/ReCap.Server/Adapters/Blaze/Component/GameManager/GameManagerComponent.cs +++ b/ReCap.Server/Adapters/Blaze/Component/GameManager/GameManagerComponent.cs @@ -1,4 +1,4 @@ -using ReCap.Server.Config; +using ReCap.Server.Config; using ReCap.Server.Services; using ReCap.Server.Util; @@ -42,6 +42,29 @@ private static bool HandleFinalizeGameCreationPacket(Client client, Packet packe var request = packet.ReadContent(); client.RespondTo(packet); + + // From C++ FinalizeGameCreation + // NotifyGameStateChange(request, gameId, GameState::InGame); + // NotifyGamePlayerStateChange(request, gameId, user->get_id(), PlayerState::Connected); + // NotifyPlayerJoinCompleted(request, gameId, user->get_id()); + + uint gameId = 1; // It's hardcoded to 1 for now + + client.Notify(new NotifyGameStateChange() { GameId = gameId, GameState = GameState.InGame }, 4, 0x64); + + client.Notify(new NotifyGamePlayerStateChange() + { + GameId = gameId, + PlayerId = client.UserId, + PlayerState = PlayerState.ActiveConnected + }, 4, 0x74); + + client.Notify(new NotifyPlayerJoinCompleted() + { + GameId = gameId, + PlayerId = client.UserId + }, 4, 0x1E); + return true; } @@ -56,7 +79,8 @@ private bool HandleResetDedicatedServer(Client client, Packet packet) return true; } - game.SetupPlayer(1, 0); + GameHandler?.AddClientToGame(client.UserId, game.Id); + game.SetupPlayer(client.UserId, 0); game.SetupBot(1); game.SetupBot(2); game.SetupBot(3); @@ -67,11 +91,18 @@ private bool HandleResetDedicatedServer(Client client, Packet packet) game.SetupBot(8); game.SetupBot(9); - // TODO: AddPlayerToGame - //gameService.AddPlayerToGame(game.Id, 1); - client.RespondTo(packet, new JoinGameResponse() { GameId = game.Id, JoinState = JoinState.JoinedGame }); + var hostAddr = new NetworkAddress() + { + ActiveMember = NetworkAddressMember.IpPairAddress, + }; + + hostAddr.IpPairAddress.InternalAddress.Ip = 0x7F000001; + hostAddr.IpPairAddress.InternalAddress.Port = 42000; + hostAddr.IpPairAddress.ExternalAddress.Ip = 0x7F000001; + hostAddr.IpPairAddress.ExternalAddress.Port = 42000; + var notify = new NotifyGameSetup(); notify.GameData.AdminPlayerList = request.AdminPlayerList; @@ -79,13 +110,12 @@ private bool HandleResetDedicatedServer(Client client, Packet packet) notify.GameData.GameAttribs = request.GameAttribs; notify.GameData.GameId = game.Id; notify.GameData.GameName = request.GameName; - notify.GameData.GameProtocolVersionHash = 1; - notify.GameData.GameReportingId = game.Id; + notify.GameData.GameProtocolVersionHash = 0xABABABAB; + notify.GameData.GameReportingId = 0xCDCDCDCD; notify.GameData.GameSettings = request.GameSettings; - notify.GameData.GameState = GameState.NewState; + notify.GameData.GameState = GameState.Initializing; notify.GameData.GameStatusURL = request.GameStatusURL; notify.GameData.GameTypeName = request.GameTypeName; - //notify.GameData.HostNetworkAddressList = request.HostNetworkAddressList; notify.GameData.IgnoreEntryCriteriaWithInvite = request.IgnoreEntryCriteriaWithInvite; notify.GameData.MeshAttribs = request.MeshAttribs; notify.GameData.ServerNotResetable = request.ServerNotResetable; @@ -100,20 +130,22 @@ private bool HandleResetDedicatedServer(Client client, Packet packet) notify.GameData.TeamIds = request.TeamIds; notify.GameData.VoipNetwork = request.VoipNetwork; notify.GameData.VersionString = request.VersionString; - notify.GameData.PlatformHostInfo.PlayerId = 1; - notify.GameData.PlatformHostInfo.SlotId = 1; - notify.GameData.TopologyHostInfo.PlayerId = 1; + notify.GameData.PlatformHostInfo.PlayerId = client.UserId; + notify.GameData.PlatformHostInfo.SlotId = 0; + notify.GameData.TopologyHostInfo.PlayerId = client.UserId; notify.GameData.TopologyHostInfo.SlotId = 0; notify.GameData.TopologyHostSessionId = 13666; + notify.GameData.PingSiteAlias = "ams"; + notify.GameData.SharedSeed = 0xFAFAFAFA; notify.GameData.UUID = "71bc4bdb-82ec-494d-8d75-ca5123b827ac"; + notify.GameData.NetworkQosData.DownstreamBitsPerSecond = 128000; + notify.GameData.NetworkQosData.NatType = NatType.Open; + notify.GameData.NetworkQosData.UpstreamBitsPerSecond = 2; notify.GameData.GameAttribs.Add("ServerBuildVersion", "1.0.903.854"); if (!notify.GameData.GameAttribs.ContainsKey("GameOwnerId")) - notify.GameData.GameAttribs.Add("GameOwnerId", "1"); - - //if (!notify.GameData.GameAttribs.ContainsKey("GameType")) - // notify.GameData.GameAttribs.Add("GameType", ""); + notify.GameData.GameAttribs.Add("GameOwnerId", client.UserId.ToString()); if (!notify.GameData.GameAttribs.ContainsKey("GameOwnerName")) notify.GameData.GameAttribs.Add("GameOwnerName", "HelloDarkspore"); @@ -127,35 +159,10 @@ private bool HandleResetDedicatedServer(Client client, Packet packet) if (!notify.GameData.GameAttribs.ContainsKey("PrivateMatch")) notify.GameData.GameAttribs.Add("PrivateMatch", "0"); - //if (!notify.GameData.GameAttribs.ContainsKey("LevelId")) - // notify.GameData.GameAttribs.Add("LevelId", "0"); + notify.GameData.HostNetworkAddressList.Add(hostAddr); - //if (!notify.GameData.GameAttribs.ContainsKey("TeamRostersKey")) - // notify.GameData.GameAttribs.Add("TeamRostersKey", ""); - - //if (!notify.GameData.GameAttribs.ContainsKey("planet")) - // notify.GameData.GameAttribs.Add("planet", ""); - - //if (!notify.GameData.GameAttribs.ContainsKey("planetDifficulty")) - // notify.GameData.GameAttribs.Add("planetDifficulty", ""); - - //if (!notify.GameData.GameAttribs.ContainsKey("RosterLockedKey")) - // notify.GameData.GameAttribs.Add("RosterLockedKey", ""); - - var addr = new NetworkAddress() - { - ActiveMember = NetworkAddressMember.IpPairAddress, - }; - - addr.IpPairAddress.InternalAddress.Ip = 0x7F000001; - addr.IpPairAddress.InternalAddress.Port = 42000; - addr.IpPairAddress.ExternalAddress.Ip = 0x7F000001; - addr.IpPairAddress.ExternalAddress.Port = 42000; - - notify.GameData.HostNetworkAddressList.Add(addr); - - if (!notify.GameData.AdminPlayerList.Contains(1)) - notify.GameData.AdminPlayerList.Add(1); + if (!notify.GameData.AdminPlayerList.Contains(client.UserId)) + notify.GameData.AdminPlayerList.Add(client.UserId); var player = new ReplicatedGamePlayer { @@ -164,21 +171,32 @@ private bool HandleResetDedicatedServer(Client client, Packet packet) GameId = game.Id, AccountLocale = 0x656E5553, PlayerName = "HelloDarkspore", - PlayerId = 1, + PlayerId = client.UserId, JoinedGameTimestamp = CurrentUnixTime, - PlayerState = PlayerState.ActiveConnected, + PlayerState = PlayerState.ActiveConnecting, TeamIndex = 0xFFFF, - PlayerSessionId = 1, + PlayerSessionId = client.UserId, }; + player.NetworkAddress.ActiveMember = NetworkAddressMember.IpPairAddress; + player.NetworkAddress.IpPairAddress.InternalAddress.Ip = 0x7F000001; + player.NetworkAddress.IpPairAddress.InternalAddress.Port = 42000; + player.NetworkAddress.IpPairAddress.ExternalAddress.Ip = 0x7F000001; + player.NetworkAddress.IpPairAddress.ExternalAddress.Port = 42000; notify.GameRoster.Add(player); notify.GameSetupReason.ActiveMember = GameSetupReasonMember.DatalessSetupContext; notify.GameSetupReason.DatalessSetupContext.SetupContext = DatalessContext.CreateGameSetupContext; + client.Notify(new NotifyGameCreated() { GameId = game.Id }, Id, 0x0F); client.Notify(notify, Id, 0x14); - client.Notify(new NotifyGameStateChange() { GameId = game.Id, GameState = GameState.Initializing }, Id, 0x64); + client.Notify(new NotifyPlayerJoining() + { + GameId = game.Id, + JoiningPlayer = player + }, Id, 0x15); + return true; } diff --git a/ReCap.Server/Config/ServerConfig.cs b/ReCap.Server/Config/ServerConfig.cs index beb0f26..cc0bb3d 100644 --- a/ReCap.Server/Config/ServerConfig.cs +++ b/ReCap.Server/Config/ServerConfig.cs @@ -32,6 +32,10 @@ public static string ServerDatabaseDirectory { get => _currentOpts.ServerDatabaseDirectory; } + public static string GamePath + { + get => _currentOpts.GamePath; + } public static readonly string ResourcesDirectory = Path.Join(AppDomain.CurrentDomain.BaseDirectory, "resources"); @@ -55,5 +59,6 @@ public static ServerConfigOptions CopyCurrentOptions() HostIP = _currentOpts.HostIP, GameVersion = _currentOpts.GameVersion, ServerDatabaseDirectory = _currentOpts.ServerDatabaseDirectory, + GamePath = _currentOpts.GamePath, }; } \ No newline at end of file diff --git a/ReCap.Server/Config/ServerConfigOptions.cs b/ReCap.Server/Config/ServerConfigOptions.cs index 160ac40..eccdcdf 100644 --- a/ReCap.Server/Config/ServerConfigOptions.cs +++ b/ReCap.Server/Config/ServerConfigOptions.cs @@ -44,6 +44,14 @@ public string ServerDatabaseDirectory set => _serverDatabaseDirectory = value; } + public static readonly string DEFAULT_GAME_PATH = string.Empty; + string _gamePath = DEFAULT_GAME_PATH; + public string GamePath + { + get => _gamePath; + set => _gamePath = value; + } + diff --git a/ReCap.Server/Models/AccountModel.cs b/ReCap.Server/Models/AccountModel.cs index b9430d1..a890620 100644 --- a/ReCap.Server/Models/AccountModel.cs +++ b/ReCap.Server/Models/AccountModel.cs @@ -1,3 +1,5 @@ +using System.ComponentModel.DataAnnotations.Schema; + namespace ReCap.Server.Models; public class AccountModel @@ -8,44 +10,45 @@ public class AccountModel public required string Username { get; set; } public required string Password { get; set; } - public required bool tutorialCompleted; - public required bool grantAllAccess; - public required bool? grantOnlineAccess; + public required bool tutorialCompleted { get; set; } + public required bool grantAllAccess { get; set; } + public bool? grantOnlineAccess { get; set; } - public required int chainProgression; - public required int creatureRewards; + public required int chainProgression { get; set; } + public required int creatureRewards { get; set; } - public required int currentGameId; - public required int currentPlaygroupId; + public required int currentGameId { get; set; } + public required int currentPlaygroupId { get; set; } - public required int defaultDeckPveId; - public required int defaultDeckPvpId; + public required int defaultDeckPveId { get; set; } + public required int defaultDeckPvpId { get; set; } - public required int level; - public required int xp; - public required int dna; - public required int avatarId; + public required int level { get; set; } + public required int xp { get; set; } + public required int dna { get; set; } + public required int avatarId { get; set; } - public required int newPlayerInventory; - public required int newPlayerProgress; + public required int newPlayerInventory { get; set; } + public required int newPlayerProgress { get; set; } - public required int cashoutBonusTime; - public required int starLevel; + public required int cashoutBonusTime { get; set; } + public required int starLevel { get; set; } - public required int unlockCatalysts; - public required int unlockDiagonalCatalysts; - public required int unlockInventory; - public required int unlockFuelTanks; - public required int unlockPveDecks; - public required int unlockPvpDecks; - public required int unlockStats; - public required int unlockInventoryIdentify; - public required int unlockEditorFlairSlots; + public required int unlockCatalysts { get; set; } + public required int unlockDiagonalCatalysts { get; set; } + public required int unlockInventory { get; set; } + public required int unlockFuelTanks { get; set; } + public required int unlockPveDecks { get; set; } + public required int unlockPvpDecks { get; set; } + public required int unlockStats { get; set; } + public required int unlockInventoryIdentify { get; set; } + public required int unlockEditorFlairSlots { get; set; } - public required int upsell; + public required int upsell { get; set; } - public required int capLevel; - public required int capProgression; + public required int capLevel { get; set; } + public required int capProgression { get; set; } - public Dictionary settings; + [NotMapped] + public Dictionary? settings { get; set; } } \ No newline at end of file diff --git a/ReCap.Server/Services/AccountService.cs b/ReCap.Server/Services/AccountService.cs index 9c670f9..c0672f7 100644 --- a/ReCap.Server/Services/AccountService.cs +++ b/ReCap.Server/Services/AccountService.cs @@ -91,13 +91,15 @@ public AccountModel createAccount(string email, string name, string password, in account.unlockPveDecks = 2; account.unlockPvpDecks = 1; account.unlockStats = 1; - account.unlockInventoryIdentify = 13; - account.unlockInventory = 3000; // 570; + account.unlockInventoryIdentify = 2500; + account.unlockInventory = 2500; account.unlockEditorFlairSlots = 1; account.upsell = 1; account.xp = 10000; account.grantAllAccess = true; account.grantOnlineAccess = true; + account.capLevel = 0; + account.capProgression = 0; } return accountRepository.insertAccount(account); diff --git a/ReCap.Server/Services/AssetDatabase.cs b/ReCap.Server/Services/AssetDatabase.cs new file mode 100644 index 0000000..102f248 --- /dev/null +++ b/ReCap.Server/Services/AssetDatabase.cs @@ -0,0 +1,41 @@ +using AssetData.Parser; +using ReCap.Server.Config; + +namespace ReCap.Server.Services; + +public sealed class AssetDatabase : IDisposable +{ + private readonly DbpfReader _reader; + private readonly AssetParser _parser = new(); + private readonly Dictionary _cache = new(StringComparer.OrdinalIgnoreCase); + + public AssetDatabase(string packagePath) + { + _reader = new DbpfReader(packagePath); + } + + public static AssetDatabase FromConfig() => new(ServerConfig.GamePath); + + public AssetNode? GetAsset(string virtualName) + { + if (_cache.TryGetValue(virtualName, out var cached)) + return cached; + + var data = _reader.GetAsset(virtualName); + if (data is null) + return null; + + var ext = Path.GetExtension(virtualName).TrimStart('.'); + var fileType = _parser.GetFileType(ext); + if (fileType is null) + return null; + + var node = _parser.Parse(data, fileType.RootStruct, fileType.HeaderSize); + _cache[virtualName] = node; + return node; + } + + public IEnumerable ListAssets() => _reader.ListAssets(); + + public void Dispose() => _reader.Dispose(); +} diff --git a/ReCap.Server/Services/GameService.cs b/ReCap.Server/Services/GameService.cs index 64e7395..a012839 100644 --- a/ReCap.Server/Services/GameService.cs +++ b/ReCap.Server/Services/GameService.cs @@ -4,35 +4,57 @@ namespace ReCap.Server.Services; -public class GameService +public class GameService : IGameHandler { public Dictionary Games { get; } = new(); // GameId -> Game public Dictionary GameAssigments { get; } = new(); // UserId -> GameId public ulong GameCounter { get; private set; } = 0x0080000000000001; - public Game CreateGame() + public AssetDatabase? Assets { get; set; } + + // ── IGameHandler ────────────────────────────────────────────────────────── + + IGame? IGameHandler.CreateGame() => CreateGame(); + IGame? IGameHandler.GetGame(ulong id) => GetGame(id); + + bool IGameHandler.AddClientToGame(ulong clientId, ulong gameId) { - var game = new Game(GameCounter++, GameType.Matched); + if (GameAssigments.ContainsKey(clientId)) return false; + GameAssigments.Add(clientId, gameId); + return true; + } - Games.Add(game.Id, game); + // ── Public API ──────────────────────────────────────────────────────────── + public Game CreateGame() + { + var game = new Game(GameCounter++, GameType.Matched, Assets); + Games.Add(game.Id, game); return game; } - public Game GetGameByPlayer(AccountModel account) => GetGame(GameAssigments.FirstOrDefault(g => g.Key == account.Id).Value); + public Game? GetGameByPlayer(AccountModel account) + { + if (!GameAssigments.TryGetValue(account.Id, out var gameId)) return null; + return GetGame(gameId); + } - public Game GetGame(ulong id) => Games.FirstOrDefault(g => g.Key == id).Value; + public Game? GetGame(ulong id) + => Games.TryGetValue(id, out var g) ? g : null; public List GetAllGames() => Games.Values.ToList(); - public bool AddPlayerToGame(ulong gameId, AccountModel account) + public bool AddPlayerToGame(ulong gameId, AccountModel account, byte slot = 0) { - if (GameAssigments.ContainsKey(account.Id)) - return false; + var game = GetGame(gameId); + if (game is null) return false; + + if (!GameAssigments.ContainsKey(account.Id)) + GameAssigments.Add(account.Id, gameId); - GameAssigments.Add(account.Id, gameId); - GetGame(gameId).AttachPlayer(account); + // Note: the player is actually attached in RakNetServer + // once they connect and send HelloPlayerRequest. return true; } } \ No newline at end of file diff --git a/lib/AssetData.Parser b/lib/AssetData.Parser new file mode 160000 index 0000000..c1f8a79 --- /dev/null +++ b/lib/AssetData.Parser @@ -0,0 +1 @@ +Subproject commit c1f8a79f7f4935bc6f98fffcaf6e9ae91375291f From 6b64c44052abad2d71af5c0384985f545684db69 Mon Sep 17 00:00:00 2001 From: JeanxPereira Date: Sat, 21 Mar 2026 15:24:11 -0300 Subject: [PATCH 5/6] Implemented RakNexus with some basic packets, Implemented AssetData.Parser, Fixed account data creation, Fix ChainVoteMsgs packet tail and LevelIndex parsing --- .../RakNet/Packets/ActionCommandMsgsPacket.cs | 100 +++++ .../Packets/ActionCommandResponsePacket.cs | 34 ++ .../RakNet/Packets/ChainPlayerMsgsPacket.cs | 61 +++ .../RakNet/Packets/ChainVoteMsgsPacket.cs | 164 ++++++++ .../RakNet/Packets/DebugPingPacket.cs | 14 + .../RakNet/Packets/DirectorStatePacket.cs | 33 ++ .../Packets/GamePrepareForStartPacket.cs | 41 +- .../RakNet/Packets/HelloPlayerPacket.cs | 10 +- .../Packets/HelloPlayerRequestPacket.cs | 8 +- .../RakNet/Packets/LabsPlayerUpdatePacket.cs | 171 +++++++++ .../Packets/LocomotionDataUpdatePacket.cs | 26 ++ .../RakNet/Packets/ObjectCreatePacket.cs | 47 +-- .../RakNet/Packets/ObjectPlayerMovePacket.cs | 35 ++ .../RakNet/Packets/ObjectUpdatePacket.cs | 15 +- .../RakNet/Packets/ObjectiveUpdatedPacket.cs | 44 +++ .../Packets/ObjectivesCompletePacket.cs | 22 ++ .../Packets/ObjectivesInitForLevelPacket.cs | 86 +++++ .../RakNet/Packets/PacketActivator.cs | 23 +- .../Packets/PartyMergeCompletePacket.cs | 31 ++ .../Packets/PlayerStatusUpdatePacket.cs | 4 +- .../RakNet/Packets/QuickGameMsgsPacket.cs | 27 ++ ReCap.Server/Adapters/RakNet/RakNetServer.cs | 8 +- .../Adapters/RakNet/ReflectionSerializer.cs | 73 ++++ .../Adapters/Rest/GameRestController.cs | 9 +- ReCap.Server/Domain/Gameplay/ChainData.cs | 147 +++++++ ReCap.Server/Domain/Gameplay/Game.cs | 359 ++++++++++++++++-- ReCap.Server/Domain/Gameplay/GameplayState.cs | 12 +- .../Gameplay/Objects/GameObjectCreateData.cs | 61 +++ .../Domain/Gameplay/Objects/LocomotionData.cs | 225 +++++++++++ .../Gameplay/Objects/SporelabsObject.cs | 120 ++++++ ReCap.Server/Program.cs | 32 +- ReCap.Server/ReCap.Server.csproj | 5 +- ReCap.Server/ReCap.Server.sln | 24 ++ ReCap.Server/Services/AssetDatabase.cs | 58 +++ ReCap.Server/Util/BigEndianExtensions.cs | 89 +++++ 35 files changed, 2098 insertions(+), 120 deletions(-) create mode 100644 ReCap.Server/Adapters/RakNet/Packets/ActionCommandMsgsPacket.cs create mode 100644 ReCap.Server/Adapters/RakNet/Packets/ActionCommandResponsePacket.cs create mode 100644 ReCap.Server/Adapters/RakNet/Packets/ChainPlayerMsgsPacket.cs create mode 100644 ReCap.Server/Adapters/RakNet/Packets/ChainVoteMsgsPacket.cs create mode 100644 ReCap.Server/Adapters/RakNet/Packets/DebugPingPacket.cs create mode 100644 ReCap.Server/Adapters/RakNet/Packets/DirectorStatePacket.cs create mode 100644 ReCap.Server/Adapters/RakNet/Packets/LabsPlayerUpdatePacket.cs create mode 100644 ReCap.Server/Adapters/RakNet/Packets/LocomotionDataUpdatePacket.cs create mode 100644 ReCap.Server/Adapters/RakNet/Packets/ObjectPlayerMovePacket.cs create mode 100644 ReCap.Server/Adapters/RakNet/Packets/ObjectiveUpdatedPacket.cs create mode 100644 ReCap.Server/Adapters/RakNet/Packets/ObjectivesCompletePacket.cs create mode 100644 ReCap.Server/Adapters/RakNet/Packets/ObjectivesInitForLevelPacket.cs create mode 100644 ReCap.Server/Adapters/RakNet/Packets/PartyMergeCompletePacket.cs create mode 100644 ReCap.Server/Adapters/RakNet/Packets/QuickGameMsgsPacket.cs create mode 100644 ReCap.Server/Adapters/RakNet/ReflectionSerializer.cs create mode 100644 ReCap.Server/Domain/Gameplay/ChainData.cs create mode 100644 ReCap.Server/Domain/Gameplay/Objects/GameObjectCreateData.cs create mode 100644 ReCap.Server/Domain/Gameplay/Objects/LocomotionData.cs create mode 100644 ReCap.Server/Domain/Gameplay/Objects/SporelabsObject.cs create mode 100644 ReCap.Server/ReCap.Server.sln create mode 100644 ReCap.Server/Util/BigEndianExtensions.cs diff --git a/ReCap.Server/Adapters/RakNet/Packets/ActionCommandMsgsPacket.cs b/ReCap.Server/Adapters/RakNet/Packets/ActionCommandMsgsPacket.cs new file mode 100644 index 0000000..a3bcf1e --- /dev/null +++ b/ReCap.Server/Adapters/RakNet/Packets/ActionCommandMsgsPacket.cs @@ -0,0 +1,100 @@ +using ReCap.Server.Util; + +namespace ReCap.Server.Adapters.RakNet.Packets; + +/// +/// ActionCommandMsgs (0x9C) — client→server: player gameplay input. +/// C++ reads: ActionCommandCommonData { u32 objectId, vec3 position, quat orientation, u8 type, u8[3] pad } +/// Then command-specific data depending on type. +/// +/// ActionCommand enum: +/// Movement=3, StopMovement=4, SwitchCharacter=5, UseCharacterAbility=7, +/// UseSquadAbility=8, CatalystPickup=9, Cancel=10, UseInteractableObject=11, +/// Dance=12, Taunt=13 +/// +public class ActionCommandMsgsPacket : IRakNetPacket +{ + public PacketType Type => PacketType.ActionCommandMsgs; + + // ── Common data (ActionCommandCommonData) ── + public uint ObjectId { get; set; } + public float PosX { get; set; } + public float PosY { get; set; } + public float PosZ { get; set; } + public float OriX { get; set; } + public float OriY { get; set; } + public float OriZ { get; set; } + public float OriW { get; set; } + public byte CommandType { get; set; } + public byte Pad1 { get; set; } + public byte Pad2 { get; set; } + public byte Pad3 { get; set; } + + // ── Remaining raw bytes for command-specific data ── + public byte[] ExtraData { get; set; } = Array.Empty(); + + public void ReadFrom(Stream stream) + { + using var reader = new BinaryReader(stream, System.Text.Encoding.UTF8, leaveOpen: true); + + ObjectId = reader.ReadUInt32BE(); + PosX = reader.ReadSingleBE(); + PosY = reader.ReadSingleBE(); + PosZ = reader.ReadSingleBE(); + OriX = reader.ReadSingleBE(); + OriY = reader.ReadSingleBE(); + OriZ = reader.ReadSingleBE(); + OriW = reader.ReadSingleBE(); + CommandType = reader.ReadByte(); + Pad1 = reader.ReadByte(); + Pad2 = reader.ReadByte(); + Pad3 = reader.ReadByte(); + + // Read remaining bytes as ExtraData + var remaining = stream.Length - stream.Position; + if (remaining > 0) + ExtraData = reader.ReadBytes((int)remaining); + } + + public void WriteTo(Stream stream) { /* server never sends this packet */ } + + // ── Helpers to read command-specific data from ExtraData ── + + /// + /// Read movement data: [u32 goalFlags] [vec3 goalPosition] + /// Used by Movement (3), StopMovement (4), Cancel (10) + /// + public (uint goalFlags, float gx, float gy, float gz) ReadMovementData() + { + using var ms = new MemoryStream(ExtraData); + using var r = new BinaryReader(ms); + return ( + r.ReadUInt32BE(), + r.ReadSingleBE(), + r.ReadSingleBE(), + r.ReadSingleBE() + ); + } + + /// + /// Read switch character data: [u32 creatureIndex] + /// Used by SwitchCharacter (5) + /// + public uint ReadSwitchIndex() + { + using var ms = new MemoryStream(ExtraData); + using var r = new BinaryReader(ms); + return r.ReadUInt32BE(); + } + + /// + /// Read interactable data: [u32 objectId] + /// Used by UseInteractableObject (11), CatalystPickup (9) + /// + public uint ReadInteractableObjectId() + { + using var ms = new MemoryStream(ExtraData); + using var r = new BinaryReader(ms); + return r.ReadUInt32BE(); + } +} diff --git a/ReCap.Server/Adapters/RakNet/Packets/ActionCommandResponsePacket.cs b/ReCap.Server/Adapters/RakNet/Packets/ActionCommandResponsePacket.cs new file mode 100644 index 0000000..752f487 --- /dev/null +++ b/ReCap.Server/Adapters/RakNet/Packets/ActionCommandResponsePacket.cs @@ -0,0 +1,34 @@ +using System.IO; +using System.Text; + +using ReCap.Server.Domain.Gameplay.Objects; +using ReCap.Server.Util; + +namespace ReCap.Server.Adapters.RakNet.Packets; + +public class ActionCommandResponsePacket : IRakNetPacket +{ + public PacketType Type => PacketType.ActionCommandResponse; + public byte ActionType { get; set; } + + public void ReadFrom(Stream stream) {} + + public void WriteTo(Stream stream) + { + using var writer = new BinaryWriter(stream, Encoding.UTF8, true); + + writer.Write((byte)0xFF); + writer.Write(ActionType); + writer.Write((byte)0xAA); + writer.Write((byte)0xBB); + + switch (ActionType) + { + case 0x08: // Move to position + stream.Position += 0x34; // 0x34 padding + break; + default: + break; + } + } +} diff --git a/ReCap.Server/Adapters/RakNet/Packets/ChainPlayerMsgsPacket.cs b/ReCap.Server/Adapters/RakNet/Packets/ChainPlayerMsgsPacket.cs new file mode 100644 index 0000000..3bbbb5a --- /dev/null +++ b/ReCap.Server/Adapters/RakNet/Packets/ChainPlayerMsgsPacket.cs @@ -0,0 +1,61 @@ +using System.IO; +using System.Text; + +using ReCap.Server.Util; + +namespace ReCap.Server.Adapters.RakNet.Packets; + +public class ChainPlayerMsgsPacket : IRakNetPacket +{ + public PacketType Type => PacketType.ChainPlayerMsgs; + + public byte Value { get; set; } + public byte Ready { get; set; } + public byte Difficulty { get; set; } + public uint LevelIndex { get; set; } + + public int ByteCount { get; set; } + + public void ReadFrom(Stream stream) + { + ByteCount = (int)(stream.Length - stream.Position); + using var reader = new BinaryReader(stream, Encoding.UTF8, true); + + switch (ByteCount) + { + case 1: + Value = reader.ReadByte(); + break; + case 2: + Value = reader.ReadByte(); + Ready = reader.ReadByte(); + break; + case 6: + Ready = reader.ReadByte(); + Difficulty = reader.ReadByte(); + LevelIndex = reader.ReadUInt32(); // read 4 bytes LittleEndian + break; + } + } + + public void WriteTo(Stream stream) + { + using var writer = new BinaryWriter(stream, Encoding.UTF8, true); + + if (ByteCount == 1) + { + writer.Write(Value); + } + else if (ByteCount == 2) + { + writer.Write(Value); + writer.Write(Ready); + } + else if (ByteCount == 6) + { + writer.Write(Ready); + writer.Write(Difficulty); + writer.Write(LevelIndex); // default BinaryWriter is LittleEndian + } + } +} diff --git a/ReCap.Server/Adapters/RakNet/Packets/ChainVoteMsgsPacket.cs b/ReCap.Server/Adapters/RakNet/Packets/ChainVoteMsgsPacket.cs new file mode 100644 index 0000000..fa32eb2 --- /dev/null +++ b/ReCap.Server/Adapters/RakNet/Packets/ChainVoteMsgsPacket.cs @@ -0,0 +1,164 @@ +using System; +using System.Buffers.Binary; +using System.IO; +using System.Text; +using ReCap.Server.Domain.Gameplay; + +namespace ReCap.Server.Adapters.RakNet.Packets; + +public class ChainVoteMsgsPacket : IRakNetPacket +{ + public PacketType Type => PacketType.ChainVoteMsgs; + + public byte Value { get; set; } + + // For Value == 0 + public ChainData? ChainData { get; set; } + + // For Value == 1 + public float SecondsUntilDeployment { get; set; } + + // For Value == 2 + public bool StayInParty { get; set; } + + public void ReadFrom(Stream stream) + { + throw new NotImplementedException("Server only sends ChainVoteMsgs, does not receive them."); + } + + public void WriteTo(Stream stream) + { + using var writer = new BinaryWriter(stream, Encoding.UTF8, true); + writer.Write(Value); + + switch (Value) + { + case 0: + if (ChainData != null) + { + WriteChainData(writer, ChainData); + } + break; + case 1: + writer.Write(SecondsUntilDeployment); + break; + case 2: + writer.Write(StayInParty); + break; + } + } + + private void WriteChainData(BinaryWriter writer, ChainData data) + { + byte[] buffer = new byte[0x170]; // enlarged to safely write trailing properties + + Action writeUInt32 = (offset, val) => BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(offset), val); + Action writeFloat = (offset, val) => BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(offset), BitConverter.SingleToUInt32Bits(val)); + + if (data.CompletedLevel) + { + writeUInt32(0x00, data.MinorDifficulty); + writeUInt32(0x04, data.MajorDifficulty); + writeFloat(0x08, 15 * 60 * 1000f); + writeFloat(0x0C, 30 * 60 * 1000f); + } + else + { + Console.WriteLine($"[ChainVoteMsgsPacket] Writing Level: {data.Level:X8}, LevelIndex: {data.LevelIndex:X8}"); + writeUInt32(0x00, data.Level); + writeUInt32(0x04, data.LevelIndex); + writeUInt32(0x08, data.StarLevel); + writeFloat(0x0C, 30 * 60 * 1000f); + } + + buffer[0x10] = data.Progression; + + for (int i = 0; i < 6; i++) + { + writeUInt32(0x11 + (i * 4), data.EnemyNouns[i]); + } + + for (int i = 0; i < 2; i++) + { + writeUInt32(0x29 + (i * 4), data.LevelNouns[i]); + } + + if (data.CompletedLevel) + { + writeUInt32(0x31, 4); + writeUInt32(0x35, 3); + } + + writeUInt32(0x39, 0); + writeUInt32(0x3D, ChainData.FnvHash("fmv_02_zelems.vp6")); + writeUInt32(0x41, ChainData.FnvHash("fmv_02_zelems.vp6")); + + writeUInt32(0x45, ChainData.FnvHash("vo_ship_flow_reinfect_zelems")); + writeUInt32(0x49, 0); + + if (data.CompletedLevel) + { + writeUInt32(0x4D, 0); + writeUInt32(0x51, data.LevelIndex); + + for (int i = 0; i < 4; ++i) + { + writeUInt32(0x55 + (i * 4), (uint)(30 + i * 30)); + } + + for (int i = 0; i < 4; ++i) + { + writeUInt32(0x65 + (i * 4), (uint)(40 + i * 40)); + } + + for (int i = 0; i < 4; ++i) + { + int offset = (i * 0x19) + 0x76; + writeUInt32(offset, (uint)(10 + i * 10)); // pve kills + writeUInt32(offset + 4, 5); // unknown + + writeFloat(offset + 8, 15f); // damage dealt + writeFloat(offset + 12, 10f); // damage taken + + writeFloat(offset + 16, 25f); // healing done + writeFloat(offset + 20, 5f); // healing received + + buffer[offset + 24] = (byte)i; + } + } + + writeUInt32(0xD9, 0); + writeUInt32(0xDD, ChainData.FnvHash("fmv_03_nocturna.vp6")); + writeUInt32(0xE1, ChainData.FnvHash("fmv_03_nocturna.vp6")); + writeUInt32(0xE5, 0); + writeUInt32(0xE9, data.Level); + + if (data.CompletedLevel) + { + for (int i = 0; i < 4; ++i) + { + int offset = (i * 0x19) + 0xEE; + writeUInt32(offset, (uint)(10 + i * 10)); // pve kills + writeUInt32(offset + 4, 5); // unknown + + writeFloat(offset + 8, 125f); // damage dealt + writeFloat(offset + 12, 1231f); // damage taken + + writeFloat(offset + 16, 515151f); // healing done + writeFloat(offset + 20, 23123f); // healing received + + buffer[offset + 24] = (byte)i; + } + } + + int tailOffset = 0xEE + (data.CompletedLevel ? (4 * 0x19) : 0); + writeUInt32(tailOffset, 10); + writeUInt32(tailOffset + 4, 20); + writeUInt32(tailOffset + 8, 30); + writeUInt32(tailOffset + 12, 40); + writeUInt32(tailOffset + 16, 50); + writeUInt32(tailOffset + 20, 60); + + writer.Write(buffer, 0, 0x151); // Write exactly the 0x151 bytes to match C++ bounds + } +} diff --git a/ReCap.Server/Adapters/RakNet/Packets/DebugPingPacket.cs b/ReCap.Server/Adapters/RakNet/Packets/DebugPingPacket.cs new file mode 100644 index 0000000..675defd --- /dev/null +++ b/ReCap.Server/Adapters/RakNet/Packets/DebugPingPacket.cs @@ -0,0 +1,14 @@ +namespace ReCap.Server.Adapters.RakNet.Packets; + +public class DebugPingPacket : IRakNetPacket +{ + public PacketType Type => PacketType.DebugPing; + + public void ReadFrom(Stream stream) + { + } + + public void WriteTo(Stream stream) + { + } +} diff --git a/ReCap.Server/Adapters/RakNet/Packets/DirectorStatePacket.cs b/ReCap.Server/Adapters/RakNet/Packets/DirectorStatePacket.cs new file mode 100644 index 0000000..e3f794e --- /dev/null +++ b/ReCap.Server/Adapters/RakNet/Packets/DirectorStatePacket.cs @@ -0,0 +1,33 @@ +using System.IO; +using System.Text; +using ReCap.Server.Util; + +namespace ReCap.Server.Adapters.RakNet.Packets; + +public class DirectorStatePacket : IRakNetPacket +{ + public PacketType Type => PacketType.DirectorState; + + public void ReadFrom(Stream stream) { } + + public void WriteTo(Stream stream) + { + using var writer = new BinaryWriter(stream, Encoding.UTF8, true); + + // C++ Server: + // void Server::SendDirectorState(...) + // outStream.Write(PacketID::DirectorState); + // director.WriteTo(outStream); + + // Director.WriteTo(outStream) implementation in C++: + // Write(stream, mEnabled); + // Write(stream, mState); + // Write(stream, mIntensityState); + // Write(stream, mIntensity); + + writer.WriteBE(1u); // Enabled + writer.WriteBE(0u); // State + writer.WriteBE(0u); // IntensityState + writer.WriteBE(0f); // Intensity + } +} diff --git a/ReCap.Server/Adapters/RakNet/Packets/GamePrepareForStartPacket.cs b/ReCap.Server/Adapters/RakNet/Packets/GamePrepareForStartPacket.cs index 561ebad..ceadc5c 100644 --- a/ReCap.Server/Adapters/RakNet/Packets/GamePrepareForStartPacket.cs +++ b/ReCap.Server/Adapters/RakNet/Packets/GamePrepareForStartPacket.cs @@ -1,4 +1,6 @@ -using System.Text; +using System.Text; + +using ReCap.Server.Util; namespace ReCap.Server.Adapters.RakNet.Packets; @@ -6,43 +8,40 @@ public class GamePrepareForStartPacket : IRakNetPacket { public PacketType Type => PacketType.GamePrepareForStart; - public uint Slot { get; set; } - public uint unk2; // 0x00000004 works - public uint pLevelAsset; // Always gonna be SM_Map_v5.Level - 0x946D41FE - public uint markerSet1; // SM_Map_v5_default.Markerset - 0x6B9636C0 works - public uint markerSet2; // SM_Map_v5_default.Markerset - 0x6B9636C0 works - public byte unk6; // 0x00 work + public uint LevelHash { get; set; } + public uint MarkerSetHash { get; set; } + public uint PlayerBitmask { get; set; } + public uint LevelIndex { get; set; } public GamePrepareForStartPacket() { } - public GamePrepareForStartPacket(uint slot) + public GamePrepareForStartPacket(uint levelHash, uint markerSetHash, uint playerBitmask, uint levelIndex) { - Slot = slot; + LevelHash = levelHash; + MarkerSetHash = markerSetHash; + PlayerBitmask = playerBitmask; + LevelIndex = levelIndex; } public void ReadFrom(Stream stream) { using var reader = new BinaryReader(stream, Encoding.UTF8, true); - Slot = reader.ReadUInt32(); - unk2 = reader.ReadUInt32(); - pLevelAsset = reader.ReadUInt32(); - markerSet1 = reader.ReadUInt32(); - markerSet2 = reader.ReadUInt32(); - unk6 = reader.ReadByte(); + LevelHash = reader.ReadUInt32(); + MarkerSetHash = reader.ReadUInt32(); + PlayerBitmask = reader.ReadUInt32(); + LevelIndex = reader.ReadUInt32(); } public void WriteTo(Stream stream) { using var writer = new BinaryWriter(stream, Encoding.UTF8, true); - writer.Write(Slot); - writer.Write(unk2); - writer.Write(pLevelAsset); - writer.Write(markerSet1); - writer.Write(markerSet2); - writer.Write(unk6); + writer.WriteBE(LevelHash); + writer.WriteBE(MarkerSetHash); + writer.WriteBE(PlayerBitmask); + writer.WriteBE(LevelIndex); } } diff --git a/ReCap.Server/Adapters/RakNet/Packets/HelloPlayerPacket.cs b/ReCap.Server/Adapters/RakNet/Packets/HelloPlayerPacket.cs index 8ee88bf..ebc6f9a 100644 --- a/ReCap.Server/Adapters/RakNet/Packets/HelloPlayerPacket.cs +++ b/ReCap.Server/Adapters/RakNet/Packets/HelloPlayerPacket.cs @@ -1,4 +1,6 @@ -using System.Text; +using System.Text; + +using ReCap.Server.Util; namespace ReCap.Server.Adapters.RakNet.Packets; @@ -17,8 +19,8 @@ public void ReadFrom(Stream stream) PlayerType = reader.ReadByte(); GameplayIndex = reader.ReadByte(); - Address = reader.ReadUInt32(); - Port = reader.ReadUInt16(); + Address = reader.ReadUInt32BE(); + Port = reader.ReadUInt16BE(); } public void WriteTo(Stream stream) @@ -27,7 +29,7 @@ public void WriteTo(Stream stream) writer.Write(PlayerType); writer.Write(GameplayIndex); - writer.Write(Address); + writer.WriteBE(Address); writer.Write(Port); } } diff --git a/ReCap.Server/Adapters/RakNet/Packets/HelloPlayerRequestPacket.cs b/ReCap.Server/Adapters/RakNet/Packets/HelloPlayerRequestPacket.cs index 431570a..fc9b2cb 100644 --- a/ReCap.Server/Adapters/RakNet/Packets/HelloPlayerRequestPacket.cs +++ b/ReCap.Server/Adapters/RakNet/Packets/HelloPlayerRequestPacket.cs @@ -1,4 +1,6 @@ -using System.Text; +using System.Text; + +using ReCap.Server.Util; namespace ReCap.Server.Adapters.RakNet.Packets; @@ -14,7 +16,9 @@ public void ReadFrom(Stream stream) using var reader = new BinaryReader(stream, Encoding.UTF8, true); UserId = reader.ReadUInt64(); - PlaygroupId = reader.ReadUInt64(); + + if (stream.Position + 8 <= stream.Length) + PlaygroupId = reader.ReadUInt64(); } public void WriteTo(Stream stream) diff --git a/ReCap.Server/Adapters/RakNet/Packets/LabsPlayerUpdatePacket.cs b/ReCap.Server/Adapters/RakNet/Packets/LabsPlayerUpdatePacket.cs new file mode 100644 index 0000000..734bf1e --- /dev/null +++ b/ReCap.Server/Adapters/RakNet/Packets/LabsPlayerUpdatePacket.cs @@ -0,0 +1,171 @@ +using System.Text; +using ReCap.Server.Util; + +namespace ReCap.Server.Adapters.RakNet.Packets; + +public class LabsPlayerUpdatePacket : IRakNetPacket +{ + public PacketType Type => PacketType.LabsPlayerUpdate; + + public byte PlayerId { get; set; } + public ushort UpdateBits { get; set; } + + public LabsPlayerData? PlayerData { get; set; } + public LabsCharacterData?[] Characters { get; set; } = new LabsCharacterData?[3]; + public LabsCatalystData?[] Catalysts { get; set; } = new LabsCatalystData?[9]; + + public const ushort CharacterBits = 1 << 0; + public const ushort CharacterMask = 0x07; + public const ushort CrystalBits = 1 << 3; + public const ushort CrystalMask = 0x07F8; + public const ushort PlayerBits = 1 << 12; + + public void ReadFrom(Stream stream) { } + + public void WriteTo(Stream stream) + { + using var writer = new BinaryWriter(stream, Encoding.UTF8, true); + + writer.Write(PlayerId); + writer.WriteBE(UpdateBits); + + if ((UpdateBits & PlayerBits) != 0 && PlayerData != null) + { + PlayerData.WriteReflection(writer); + } + + if ((UpdateBits & CharacterMask) != 0) + { + for (int i = 0; i < 3; i++) + { + if ((UpdateBits & (CharacterBits << i)) != 0 && Characters[i] != null) + { + Characters[i]!.WriteReflection(writer); + } + } + } + + if ((UpdateBits & CrystalMask) != 0) + { + for (int i = 0; i < 9; i++) + { + if ((UpdateBits & (CrystalBits << i)) != 0 && Catalysts[i] != null) + { + Catalysts[i]!.WriteReflection(writer); + } + } + } + } +} + +public class LabsPlayerData +{ + public bool DataSetup { get; set; } + public int CurrentDeckIndex { get; set; } + public int QueuedDeckIndex { get; set; } + public byte PlayerIndex { get; set; } + public byte Team { get; set; } = 1; + public ulong PlayerOnlineId { get; set; } + public uint Status { get; set; } + public float StatusProgress { get; set; } + public uint CurrentCreatureId { get; set; } + public float EnergyPoints { get; set; } + public bool IsCharged { get; set; } + public int DNA { get; set; } + public bool LockCamera { get; set; } + public bool LockedOverdrive { get; set; } + public bool LockedCrystals { get; set; } + public uint LockedAbilityMin { get; set; } = 0xFF; + public uint LockedDeckIndexMin { get; set; } = 0xFF; + public uint DeckScore { get; set; } + public uint AvatarLevel { get; set; } + public float AvatarXP { get; set; } + public uint ChainProgression { get; set; } + + public void WriteReflection(BinaryWriter writer) + { + var reflector = new ReflectionSerializer(writer, 24); + reflector.Begin(); + + reflector.Write(0, () => writer.WriteBE(DataSetup)); + reflector.Write(1, () => writer.WriteBE(CurrentDeckIndex)); + reflector.Write(2, () => writer.WriteBE(QueuedDeckIndex)); + reflector.Write(4, () => writer.Write(PlayerIndex)); + reflector.Write(5, () => writer.Write(Team)); + reflector.Write(6, () => writer.WriteBE(PlayerOnlineId)); + reflector.Write(7, () => writer.WriteBE(Status)); + reflector.Write(8, () => writer.WriteBE(StatusProgress)); + reflector.Write(15, () => writer.WriteBE(AvatarLevel)); + reflector.Write(16, () => writer.WriteBE(AvatarXP)); + reflector.Write(17, () => writer.WriteBE(ChainProgression)); + reflector.Write(18, () => writer.WriteBE(LockCamera)); + reflector.Write(19, () => writer.WriteBE(LockedOverdrive)); + reflector.Write(20, () => writer.WriteBE(LockedCrystals)); + reflector.Write(21, () => writer.WriteBE(LockedAbilityMin)); + reflector.Write(22, () => writer.WriteBE(LockedDeckIndexMin)); + reflector.Write(23, () => writer.WriteBE(DeckScore)); + reflector.Write(12, () => writer.WriteBE(DNA)); + + reflector.End(); + } +} + +public class LabsCharacterData +{ + public int Version { get; set; } + public uint NounId { get; set; } + public ulong AssetId { get; set; } + public uint CreatureType { get; set; } + public ulong DeployCooldown { get; set; } + public uint AbilityPoints { get; set; } = 10; + public uint[] AbilityRanks { get; set; } = new uint[9]; + public float Health { get; set; } = 200f; + public float MaxHealth { get; set; } = 200f; + public float Mana { get; set; } = 200f; + public float MaxMana { get; set; } = 200f; + public float GearScore { get; set; } = 300f; + public float GearScoreFlattened { get; set; } = 300f; + + public void WriteReflection(BinaryWriter writer) + { + var reflector = new ReflectionSerializer(writer, 124); + reflector.Begin(); + + reflector.Write(0, () => writer.WriteBE(Version)); + reflector.Write(1, () => writer.WriteBE(NounId)); + reflector.Write(2, () => writer.WriteBE(AssetId)); + reflector.Write(3, () => writer.WriteBE(CreatureType)); + reflector.Write(4, () => writer.WriteBE(DeployCooldown)); + reflector.Write(5, () => writer.WriteBE(AbilityPoints)); + reflector.Write(6, () => + { + foreach (var rank in AbilityRanks) + writer.WriteBE(rank); + }); + reflector.Write(7, () => writer.WriteBE(Health)); + reflector.Write(8, () => writer.WriteBE(MaxHealth)); + reflector.Write(9, () => writer.WriteBE(Mana)); + reflector.Write(10, () => writer.WriteBE(MaxMana)); + reflector.Write(11, () => writer.WriteBE(GearScore)); + reflector.Write(12, () => writer.WriteBE(GearScoreFlattened)); + + reflector.End(); + } +} + +public class LabsCatalystData +{ + public uint NounId { get; set; } + public ushort Rarity { get; set; } + + public void WriteReflection(BinaryWriter writer) + { + var reflector = new ReflectionSerializer(writer, 2); + reflector.Begin(); + + reflector.Write(0, () => writer.WriteBE(NounId)); + reflector.Write(1, () => writer.WriteBE(Rarity)); + + reflector.End(); + } +} diff --git a/ReCap.Server/Adapters/RakNet/Packets/LocomotionDataUpdatePacket.cs b/ReCap.Server/Adapters/RakNet/Packets/LocomotionDataUpdatePacket.cs new file mode 100644 index 0000000..600f82e --- /dev/null +++ b/ReCap.Server/Adapters/RakNet/Packets/LocomotionDataUpdatePacket.cs @@ -0,0 +1,26 @@ +using System.IO; +using System.Text; + +using ReCap.Server.Domain.Gameplay.Objects; +using ReCap.Server.Util; + +namespace ReCap.Server.Adapters.RakNet.Packets; + +public class LocomotionDataUpdatePacket : IRakNetPacket +{ + public PacketType Type => PacketType.LocomotionDataUpdate; + public uint ObjectId { get; set; } + public LocomotionData Locomotion { get; set; } + + public void ReadFrom(Stream stream) + { + } + + public void WriteTo(Stream stream) + { + using var writer = new BinaryWriter(stream, Encoding.UTF8, true); + + writer.WriteBE(ObjectId); + Locomotion?.WriteTo(stream); + } +} diff --git a/ReCap.Server/Adapters/RakNet/Packets/ObjectCreatePacket.cs b/ReCap.Server/Adapters/RakNet/Packets/ObjectCreatePacket.cs index c18e44d..64f58bc 100644 --- a/ReCap.Server/Adapters/RakNet/Packets/ObjectCreatePacket.cs +++ b/ReCap.Server/Adapters/RakNet/Packets/ObjectCreatePacket.cs @@ -1,54 +1,31 @@ -using System.Numerics; +using System.IO; using System.Text; -namespace ReCap.Server.Adapters.RakNet.Packets; +using ReCap.Server.Domain.Gameplay.Objects; +using ReCap.Server.Util; -public struct ObjectCreationData -{ - public uint noun; - public Vector3 position; - public float rotXDegrees; - public float rotYDegrees; - public float rotZDegrees; - public float scale; - public byte team; - public uint ownerId; - public byte flags; -} +namespace ReCap.Server.Adapters.RakNet.Packets; public class ObjectCreatePacket : IRakNetPacket { public PacketType Type => PacketType.ObjectCreate; public uint ObjectId { get; set; } - public ObjectCreationData objectCreationData { get; set; } + public GameObjectCreateData CreateData { get; set; } = new(); + public SporelabsObject ObjectData { get; set; } = new(); public void ReadFrom(Stream stream) { - using var reader = new BinaryReader(stream, Encoding.UTF8, true); - - ObjectId = reader.ReadUInt32(); - - /* todo reflection read*/ + // Server does not receive this packet } + public void WriteTo(Stream stream) { using var writer = new BinaryWriter(stream, Encoding.UTF8, true); - writer.Write(ObjectId); - - // This should be handled by the reflection code eventually - writer.Write((ushort)0x1F2F); - writer.Write(objectCreationData.noun); - writer.Write(objectCreationData.position.X); - writer.Write(objectCreationData.position.Y); - writer.Write(objectCreationData.position.Z); - writer.Write(objectCreationData.rotXDegrees); - writer.Write(objectCreationData.rotYDegrees); - writer.Write(objectCreationData.rotZDegrees); - writer.Write(objectCreationData.scale); - writer.Write(objectCreationData.team); - writer.Write(objectCreationData.ownerId); - writer.Write(objectCreationData.flags); + writer.WriteBE(ObjectId); + + CreateData.WriteReflection(stream); + ObjectData.WriteReflection(stream); } } diff --git a/ReCap.Server/Adapters/RakNet/Packets/ObjectPlayerMovePacket.cs b/ReCap.Server/Adapters/RakNet/Packets/ObjectPlayerMovePacket.cs new file mode 100644 index 0000000..9bfabb7 --- /dev/null +++ b/ReCap.Server/Adapters/RakNet/Packets/ObjectPlayerMovePacket.cs @@ -0,0 +1,35 @@ +using System.IO; +using System.Text; + +using ReCap.Server.Domain.Gameplay.Objects; +using ReCap.Server.Util; + +namespace ReCap.Server.Adapters.RakNet.Packets; + +public class ObjectPlayerMovePacket : IRakNetPacket +{ + public PacketType Type => PacketType.ObjectPlayerMove; + public uint ObjectId { get; set; } + public LocomotionData Locomotion { get; set; } + + public void ReadFrom(Stream stream) + { + // Typically not sent from client in this direction, but handled + } + + public void WriteTo(Stream stream) + { + using var writer = new BinaryWriter(stream, Encoding.UTF8, true); + + writer.WriteBE(ObjectId); + writer.WriteBE(Locomotion.GoalFlags); + writer.WriteBE(Locomotion.GoalPosition); + writer.WriteBE(Locomotion.Facing); + writer.WriteBE(Locomotion.ExternalLinearVelocity); + writer.WriteBE(Locomotion.ExternalForce); + writer.WriteBE(Locomotion.AllowedStopDistance); + writer.WriteBE(Locomotion.DesiredStopDistance); + writer.WriteBE(Locomotion.TargetPosition); + writer.WriteBE(Locomotion.TargetObjectId); + } +} diff --git a/ReCap.Server/Adapters/RakNet/Packets/ObjectUpdatePacket.cs b/ReCap.Server/Adapters/RakNet/Packets/ObjectUpdatePacket.cs index df87b6c..3034e37 100644 --- a/ReCap.Server/Adapters/RakNet/Packets/ObjectUpdatePacket.cs +++ b/ReCap.Server/Adapters/RakNet/Packets/ObjectUpdatePacket.cs @@ -1,4 +1,8 @@ -using System.Text; +using System.IO; +using System.Text; + +using ReCap.Server.Domain.Gameplay.Objects; +using ReCap.Server.Util; namespace ReCap.Server.Adapters.RakNet.Packets; @@ -6,15 +10,18 @@ public class ObjectUpdatePacket : IRakNetPacket { public PacketType Type => PacketType.ObjectUpdate; public uint ObjectId { get; set; } + public SporelabsObject ObjectData { get; set; } public void ReadFrom(Stream stream) { - using var reader = new BinaryReader(stream, Encoding.UTF8, true); - ObjectId = reader.ReadUInt32(); + // Server typically does not read ObjectUpdate from Client } + public void WriteTo(Stream stream) { using var writer = new BinaryWriter(stream, Encoding.UTF8, true); - writer.Write(ObjectId); + writer.WriteBE(ObjectId); + + ObjectData?.WriteReflection(stream); } } \ No newline at end of file diff --git a/ReCap.Server/Adapters/RakNet/Packets/ObjectiveUpdatedPacket.cs b/ReCap.Server/Adapters/RakNet/Packets/ObjectiveUpdatedPacket.cs new file mode 100644 index 0000000..fa34c4a --- /dev/null +++ b/ReCap.Server/Adapters/RakNet/Packets/ObjectiveUpdatedPacket.cs @@ -0,0 +1,44 @@ +using ReCap.Server.Util; + +namespace ReCap.Server.Adapters.RakNet.Packets; + +/// +/// ObjectiveUpdated (0xB8) — updates a single objective's progress. +/// C++: SendObjectiveUpdate +/// +/// Format: +/// [u32 objectiveId] +/// [u8 clientId] // player/client id +/// [u8 medal] // ObjectiveMedal enum: 0=InProgress,1=Failed,2=Bronze,3=Silver,4=Gold +/// [u32 voiceover] // hash_id of VO clip (0 = none) +/// [bool showNotif] // whether to show notification +/// [u32 value] // current progress value +/// [u32 unk1] // always 2 +/// [u32 unk2] // always 3 +/// +public class ObjectiveUpdatedPacket : IRakNetPacket +{ + public PacketType Type => PacketType.ObjectiveUpdated; + + public uint ObjectiveId { get; set; } + public byte ClientId { get; set; } + public byte Medal { get; set; } // 0 = InProgress + public uint Voiceover { get; set; } = 0; + public bool ShowNotif { get; set; } = false; + public uint Value { get; set; } = 0; + + public void ReadFrom(Stream stream) { } + + public void WriteTo(Stream stream) + { + using var writer = new BinaryWriter(stream, System.Text.Encoding.UTF8, leaveOpen: true); + writer.WriteBE(ObjectiveId); + stream.WriteByte(ClientId); + stream.WriteByte(Medal); + writer.WriteBE(Voiceover); + stream.WriteByte(ShowNotif ? (byte)1 : (byte)0); + writer.WriteBE(Value); + writer.WriteBE((uint)2); // unk1 + writer.WriteBE((uint)3); // unk2 + } +} diff --git a/ReCap.Server/Adapters/RakNet/Packets/ObjectivesCompletePacket.cs b/ReCap.Server/Adapters/RakNet/Packets/ObjectivesCompletePacket.cs new file mode 100644 index 0000000..76e9d7d --- /dev/null +++ b/ReCap.Server/Adapters/RakNet/Packets/ObjectivesCompletePacket.cs @@ -0,0 +1,22 @@ +namespace ReCap.Server.Adapters.RakNet.Packets; + +/// +/// ObjectivesComplete (0xB9) — signals level completion with cashout data. +/// C++: SendObjectivesComplete writes CashOutData (0x2C8 bytes). +/// +public class ObjectivesCompletePacket : IRakNetPacket +{ + public PacketType Type => PacketType.ObjectivesComplete; + + /// + /// Raw CashOutData (0x2C8 = 712 bytes). + /// + public byte[] CashOutData { get; set; } = new byte[0x2C8]; + + public void ReadFrom(Stream stream) { } + + public void WriteTo(Stream stream) + { + stream.Write(CashOutData, 0, CashOutData.Length); + } +} diff --git a/ReCap.Server/Adapters/RakNet/Packets/ObjectivesInitForLevelPacket.cs b/ReCap.Server/Adapters/RakNet/Packets/ObjectivesInitForLevelPacket.cs new file mode 100644 index 0000000..355ab5d --- /dev/null +++ b/ReCap.Server/Adapters/RakNet/Packets/ObjectivesInitForLevelPacket.cs @@ -0,0 +1,86 @@ +using ReCap.Server.Util; + +namespace ReCap.Server.Adapters.RakNet.Packets; + +/// +/// ObjectivesInitForLevel (0xB7) — sends the list of level objectives to the client. +/// C++: SendObjectivesInitForLevel → [u8 count] [for each: Objective::WriteTo] +/// +/// Objective::WriteTo format: +/// [u32 id] [u32 value] [0x40 bytes padding/debug data] +/// Total per objective: 8 + 64 = 72 bytes +/// +public class ObjectivesInitForLevelPacket : IRakNetPacket +{ + public PacketType Type => PacketType.ObjectivesInitForLevel; + + public List Objectives { get; } = new(); + + public void ReadFrom(Stream stream) { } + + public void WriteTo(Stream stream) + { + stream.WriteByte((byte)Objectives.Count); + foreach (var obj in Objectives) + obj.WriteTo(stream); + } + + /// + /// Create the default 5-objective set matching C++ Instance constructor. + /// + public static ObjectivesInitForLevelPacket CreateDefault() + { + var packet = new ObjectivesInitForLevelPacket(); + + uint[] ids = + { + FnvHash("FinishLevelQuickly"), + FnvHash("DoDamageOften"), + FnvHash("TouchAllObelisks"), + FnvHash("DefeatAllMonsters"), + FnvHash("HugeDamage"), + }; + + foreach (var id in ids) + packet.Objectives.Add(new ObjectiveData { Id = id, Value = 1 }); + + return packet; + } + + // FNV-1 matching C++ utils::hash_id (multiply THEN xor, lowercase) + private static uint FnvHash(string s) + { + uint h = 0x811C9DC5; + foreach (var c in s) { h *= 0x01000193; h ^= (byte)char.ToLower(c); } + return h; + } +} + +/// +/// Per-objective data. Mirrors C++ Objective::WriteTo. +/// +public class ObjectiveData +{ + public uint Id { get; set; } + public uint Value { get; set; } + + // C++ debug pattern: 0x01,0x23,0x45,0x67,0x89,0xAB,0xCD,0xEF repeating for 0x40 bytes + private static readonly byte[] DebugPadding = BuildDebugPadding(0x40); + + public void WriteTo(Stream stream) + { + using var writer = new BinaryWriter(stream, System.Text.Encoding.UTF8, leaveOpen: true); + writer.WriteBE(Id); + writer.WriteBE(Value); + stream.Write(DebugPadding, 0, DebugPadding.Length); + } + + private static byte[] BuildDebugPadding(int length) + { + byte[] pattern = { 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF }; + var result = new byte[length]; + for (int i = 0; i < length; i++) + result[i] = pattern[i & 7]; + return result; + } +} diff --git a/ReCap.Server/Adapters/RakNet/Packets/PacketActivator.cs b/ReCap.Server/Adapters/RakNet/Packets/PacketActivator.cs index 8f34c3f..6ee860b 100644 --- a/ReCap.Server/Adapters/RakNet/Packets/PacketActivator.cs +++ b/ReCap.Server/Adapters/RakNet/Packets/PacketActivator.cs @@ -1,4 +1,4 @@ -namespace ReCap.Server.Adapters.RakNet.Packets; +namespace ReCap.Server.Adapters.RakNet.Packets; public static class PacketActivator { @@ -35,6 +35,7 @@ public static class PacketActivator break; case PacketType.PartyMergeComplete: + packet = new PartyMergeCompletePacket(); break; case PacketType.PlayerDeparted: @@ -53,8 +54,7 @@ public static class PacketActivator case PacketType.GameState: break; - case PacketType.DirectorState: - break; + case PacketType.ObjectCreate: packet = new ObjectCreatePacket(); @@ -74,6 +74,7 @@ public static class PacketActivator break; case PacketType.ObjectPlayerMove: + packet = new ObjectPlayerMovePacket(); break; case PacketType.ForcePhysicsUpdate: @@ -83,6 +84,7 @@ public static class PacketActivator break; case PacketType.LocomotionDataUpdate: + packet = new LocomotionDataUpdatePacket(); break; case PacketType.LocomotionDataUnreliableUpdate: @@ -107,6 +109,7 @@ public static class PacketActivator break; case PacketType.ActionCommandMsgs: + packet = new ActionCommandMsgsPacket(); break; case PacketType.PlayerDamage: @@ -119,7 +122,7 @@ public static class PacketActivator break; case PacketType.LabsPlayerUpdate: - break; + return new LabsPlayerUpdatePacket(); case PacketType.ModifierCreated: break; @@ -136,13 +139,18 @@ public static class PacketActivator case PacketType.SetObjectGfxState: break; + case PacketType.DirectorState: + return new DirectorStatePacket(); + case PacketType.PlayerCharacterDeploy: break; case PacketType.ActionCommandResponse: + packet = new ActionCommandResponsePacket(); break; case PacketType.ChainVoteMsgs: + packet = new ChainVoteMsgsPacket(); break; case PacketType.ChainLevelResultsMsgs: @@ -152,6 +160,7 @@ public static class PacketActivator break; case PacketType.ChainPlayerMsgs: + packet = new ChainPlayerMsgsPacket(); break; case PacketType.ChainGameMsgs: @@ -161,7 +170,7 @@ public static class PacketActivator break; case PacketType.QuickGameMsgs: - break; + return new QuickGameMsgsPacket(); case PacketType.GamePrepareForStart: packet = new GamePrepareForStartPacket(); @@ -187,12 +196,15 @@ public static class PacketActivator break; case PacketType.ObjectivesInitForLevel: + packet = new ObjectivesInitForLevelPacket(); break; case PacketType.ObjectiveUpdated: + packet = new ObjectiveUpdatedPacket(); break; case PacketType.ObjectivesComplete: + packet = new ObjectivesCompletePacket(); break; case PacketType.CombatEvent: @@ -250,6 +262,7 @@ public static class PacketActivator break; case PacketType.DebugPing: + packet = new DebugPingPacket(); break; default: diff --git a/ReCap.Server/Adapters/RakNet/Packets/PartyMergeCompletePacket.cs b/ReCap.Server/Adapters/RakNet/Packets/PartyMergeCompletePacket.cs new file mode 100644 index 0000000..1d4856e --- /dev/null +++ b/ReCap.Server/Adapters/RakNet/Packets/PartyMergeCompletePacket.cs @@ -0,0 +1,31 @@ +using System.Text; + +using ReCap.Server.Util; + +namespace ReCap.Server.Adapters.RakNet.Packets; + +public class PartyMergeCompletePacket : IRakNetPacket +{ + public PacketType Type => PacketType.PartyMergeComplete; + + public ulong Timestamp { get; set; } + + public PartyMergeCompletePacket() + { + Timestamp = (ulong)DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + } + + public void ReadFrom(Stream stream) + { + using var reader = new BinaryReader(stream, Encoding.UTF8, true); + + Timestamp = reader.ReadUInt64BE(); + } + + public void WriteTo(Stream stream) + { + using var writer = new BinaryWriter(stream, Encoding.UTF8, true); + + writer.WriteBE(Timestamp); + } +} diff --git a/ReCap.Server/Adapters/RakNet/Packets/PlayerStatusUpdatePacket.cs b/ReCap.Server/Adapters/RakNet/Packets/PlayerStatusUpdatePacket.cs index e47064d..2cc339b 100644 --- a/ReCap.Server/Adapters/RakNet/Packets/PlayerStatusUpdatePacket.cs +++ b/ReCap.Server/Adapters/RakNet/Packets/PlayerStatusUpdatePacket.cs @@ -1,4 +1,6 @@ -using System.Text; +using System.Text; + +using ReCap.Server.Util; namespace ReCap.Server.Adapters.RakNet.Packets; diff --git a/ReCap.Server/Adapters/RakNet/Packets/QuickGameMsgsPacket.cs b/ReCap.Server/Adapters/RakNet/Packets/QuickGameMsgsPacket.cs new file mode 100644 index 0000000..db44855 --- /dev/null +++ b/ReCap.Server/Adapters/RakNet/Packets/QuickGameMsgsPacket.cs @@ -0,0 +1,27 @@ +using System.IO; +using System.Text; +using ReCap.Server.Util; + +namespace ReCap.Server.Adapters.RakNet.Packets; + +public class QuickGameMsgsPacket : IRakNetPacket +{ + public PacketType Type => PacketType.QuickGameMsgs; + + public void ReadFrom(Stream stream) { } + + public void WriteTo(Stream stream) + { + using var writer = new BinaryWriter(stream, Encoding.UTF8, true); + + // C++ Server: + // This packet contains setup messages for Quick Game + // For now, sending an empty payload or minimal payload + // C++ implementation in QuickGameMsgs just writes the Packet ID in many cases, + // or a default value. + // I will write a 0 byte to signify no extended messages, or just nothing. + // Let's check Darkspore decomp: QuickGameMsgs usually has a message type byte. + // Send a 0 type (None). + writer.Write((byte)0); + } +} diff --git a/ReCap.Server/Adapters/RakNet/RakNetServer.cs b/ReCap.Server/Adapters/RakNet/RakNetServer.cs index e2ccf9e..a8185d3 100644 --- a/ReCap.Server/Adapters/RakNet/RakNetServer.cs +++ b/ReCap.Server/Adapters/RakNet/RakNetServer.cs @@ -14,6 +14,7 @@ public class RakNetServer { private AccountService accountService; private GameService gameService; + private AssetDatabase? assetDatabase; public Dictionary Clients { get; } = new(); public Dictionary Games { get; } = new(); @@ -23,10 +24,11 @@ public class RakNetServer public bool IsRunning { get; private set; } public ulong GameCounter { get; private set; } = 0x0080000000000001; - public RakNetServer(SqliteConfig newSqliteConfig, string name, IPAddress hostAddress, int port, bool isSecure, string hostname) + public RakNetServer(SqliteConfig newSqliteConfig, string name, IPAddress hostAddress, int port, bool isSecure, string hostname, AssetDatabase? assetDatabase = null, GameService? sharedGameService = null) { accountService = new AccountService(newSqliteConfig); - gameService = new GameService(); + gameService = sharedGameService ?? new GameService(); + this.assetDatabase = assetDatabase; Listener = new RakNetListener(port); Listener.SessionConnected += OnSessionConnected; @@ -93,6 +95,8 @@ private void OnSessionReceiveRaw(RakNetSession session, Packet rakPacket) gameService.AddPlayerToGame(game.Id, account); } + client.Game = game; + game.AttachPlayer(account, client); return; } diff --git a/ReCap.Server/Adapters/RakNet/ReflectionSerializer.cs b/ReCap.Server/Adapters/RakNet/ReflectionSerializer.cs new file mode 100644 index 0000000..341ad33 --- /dev/null +++ b/ReCap.Server/Adapters/RakNet/ReflectionSerializer.cs @@ -0,0 +1,73 @@ +using System; +using System.IO; + +using ReCap.Server.Util; + +namespace ReCap.Server.Adapters.RakNet; + +public class ReflectionSerializer +{ + private readonly BinaryWriter _writer; + private readonly int _fieldCount; + private long _startOffset; + private ushort _writeBits; + + public ReflectionSerializer(BinaryWriter writer, int fieldCount) + { + if (fieldCount is <= 0 or > 255) + throw new ArgumentOutOfRangeException(nameof(fieldCount)); + + _writer = writer; + _fieldCount = fieldCount; + _startOffset = -1; + _writeBits = 0; + } + + public void Begin() + { + if (_startOffset != -1) return; + + _startOffset = _writer.BaseStream.Position; + _writeBits = 0; + + if (_fieldCount <= 8) + _writer.Write((byte)0); + else if (_fieldCount <= 16) + _writer.Write((ushort)0); + } + + public void End() + { + if (_fieldCount > 16) + { + _writer.Write((byte)0xFF); + } + else + { + var endOffset = _writer.BaseStream.Position; + _writer.BaseStream.Position = _startOffset; + + if (_fieldCount <= 8) + _writer.Write((byte)_writeBits); + else + _writer.WriteBE(_writeBits); + + _writer.BaseStream.Position = endOffset; + } + + _startOffset = -1; + } + + public void Write(byte field, Action writeAction) + { + if (field >= _fieldCount) + throw new ArgumentOutOfRangeException(nameof(field)); + + if (_fieldCount > 16) + _writer.Write(field); + else + _writeBits |= (ushort)(1 << field); + + writeAction(); + } +} diff --git a/ReCap.Server/Adapters/Rest/GameRestController.cs b/ReCap.Server/Adapters/Rest/GameRestController.cs index 5d68593..95d9d45 100644 --- a/ReCap.Server/Adapters/Rest/GameRestController.cs +++ b/ReCap.Server/Adapters/Rest/GameRestController.cs @@ -182,7 +182,8 @@ public byte[] getPlayerAccount(HttpListenerContext context, Dictionary parameters) { - return null; + // Darkspore sends this on new accounts expecting an auth response. Redirect it to auth handler. + if (parameters.TryGetValue("token", out string token)) { + parameters["key"] = $"{token}::0"; + } + return loginPlayerAccount(context, parameters); } [RequestMapping(Name="api.creature.getCreature")] diff --git a/ReCap.Server/Domain/Gameplay/ChainData.cs b/ReCap.Server/Domain/Gameplay/ChainData.cs new file mode 100644 index 0000000..619c272 --- /dev/null +++ b/ReCap.Server/Domain/Gameplay/ChainData.cs @@ -0,0 +1,147 @@ +namespace ReCap.Server.Domain.Gameplay; + +public class ChainData +{ + public static readonly string[] LevelNames = + [ + "Darkspore_Tutorial_cryos_1_v2", + "zelems_1", "zelems_3", "nocturna_4", "nocturna_1", + "verdanth_1", "verdanth_3", "zelems_2", "zelems_4", + "cryos_4", "cryos_3", "verdanth_2", "verdanth_4", + "infinity_2", "infinity_3", "cryos_1", "cryos_2", + "nocturna_3", "nocturna_2", "infinity_1", "infinity_4", + "scaldron_1", "scaldron_2", "scaldron_3", "scaldron_4" + ]; + + public uint Level { get; set; } + public uint LevelIndex { get; set; } + public uint StarLevel { get; set; } + public byte Progression { get; set; } + public bool CompletedLevel { get; set; } + + public uint[] EnemyNouns { get; } = new uint[6]; + public uint[] LevelNouns { get; } = new uint[2]; + + public ChainData() + { + SetLevelByIndex(1); // zelems_1 instead of tutorial + + EnemyNouns[0] = FnvHash("VerdanthBasicMelee.Noun"); + EnemyNouns[1] = FnvHash("ZelemBasicHybrid.Noun"); + EnemyNouns[2] = FnvHash("ZelemBasicPackMelee.Noun"); + EnemyNouns[3] = FnvHash("ZelemBasicRangedHoming.Noun"); + EnemyNouns[4] = FnvHash("ZelemBasicFlyingMelee.Noun"); + } + + public void PopulateFromLevel(ReCap.Server.Services.AssetDatabase db) + { + var levelAsset = db.GetAsset(LevelName + ".level"); + if (levelAsset == null) + return; + + var planetConfigKey = levelAsset["planetConfig"]?.DisplayValue; + if (string.IsNullOrEmpty(planetConfigKey)) + return; + + var planetConfig = db.GetAsset(planetConfigKey); + if (planetConfig == null) + return; + + var minions = planetConfig["minion"]?.Elements; + var specials = planetConfig["special"]?.Elements; + + int enemyIndex = 0; + void AddToEnemyNouns(System.Collections.Generic.IEnumerable? elements) + { + if (elements == null) return; + foreach (var el in elements) + { + if (enemyIndex >= EnemyNouns.Length) break; + var nounNode = el["mpNoun"] as AssetData.Parser.StringNode; + if (nounNode != null && !string.IsNullOrEmpty(nounNode.Value)) + { + if (!nounNode.Value.Contains("_")) + { + EnemyNouns[enemyIndex++] = FnvHash(nounNode.Value); + } + } + } + } + + AddToEnemyNouns(minions); + AddToEnemyNouns(specials); + + var bosses = planetConfig["boss"]?.Elements; + var agents = planetConfig["agent"]?.Elements; + + int levelIndex = 0; + void AddToLevelNouns(System.Collections.Generic.IEnumerable? elements) + { + if (elements == null) return; + foreach (var el in elements) + { + if (levelIndex >= LevelNouns.Length) break; + var nounNode = el["mpNoun"] as AssetData.Parser.StringNode; + if (nounNode != null && !string.IsNullOrEmpty(nounNode.Value)) + { + if (!nounNode.Value.Contains("_")) + { + LevelNouns[levelIndex++] = FnvHash(nounNode.Value); + } + } + } + } + + AddToLevelNouns(bosses); + AddToLevelNouns(agents); + } + + public string LevelName => LevelIndex < LevelNames.Length ? LevelNames[LevelIndex] : LevelNames[0]; + + public uint MarkerSet => FnvHash($"{LevelName}_ai_1.Markerset"); + + public uint MinorDifficulty => LevelIndex > 0 ? ((LevelIndex - 1) % 4) + 1 : 0; + public uint MajorDifficulty => LevelIndex > 0 ? ((LevelIndex - 1) / 4) + 1 : 0; + + public void SetLevelByIndex(int index) + { + if (index < 0 || index >= LevelNames.Length) + return; + + LevelIndex = (uint)index; + + // The first level is the tutorial. Its asset name is Darkspore_Tutorial_cryos_1_v2, + // but the UI localization hash it expects is purely "tutorial.Level" + if (index == 0) + { + Level = FnvHash("tutorial.Level"); + } + else + { + Level = FnvHash($"{LevelNames[index]}.Level"); + } + } + + public void SetEnemyNoun(string nounStr, int index) + { + if (index < EnemyNouns.Length) + EnemyNouns[index] = FnvHash(nounStr); + } + + public void SetLevelNoun(string nounStr, int index) + { + if (index < LevelNouns.Length) + LevelNouns[index] = FnvHash(nounStr); + } + + public static uint FnvHash(string s) + { + uint hash = 0x811C9DC5; + foreach (char c in s.ToLowerInvariant()) + { + hash *= 0x1000193; + hash ^= (byte)c; + } + return hash; + } +} diff --git a/ReCap.Server/Domain/Gameplay/Game.cs b/ReCap.Server/Domain/Gameplay/Game.cs index 4fde9c9..d85e97d 100644 --- a/ReCap.Server/Domain/Gameplay/Game.cs +++ b/ReCap.Server/Domain/Gameplay/Game.cs @@ -1,12 +1,16 @@ -using ReCap.Server.Adapters.RakNet; +using System.Numerics; +using AssetData.Parser; +using ReCap.Server.Adapters.RakNet; using ReCap.Server.Adapters.RakNet.Packets; +using ReCap.Server.Domain.Gameplay.Objects; using ReCap.Server.Adapters.Blaze.Component.GameManager; using ReCap.Server.Models; +using ReCap.Server.Services; namespace ReCap.Server.Domain.Gameplay; -public class Game(ulong id, GameType gameType) : IGame +public class Game(ulong id, GameType gameType, AssetDatabase? assetDatabase = null) : IGame { public Dictionary ExpectedPlayers { get; } = new(); public Dictionary Players { get; } = new(); @@ -16,11 +20,16 @@ public class Game(ulong id, GameType gameType) : IGame public Dictionary ClientsBySlot { get; } = new(); public DateTime StartTime { get; } = DateTime.UtcNow; - public ulong Id { get; } + public ulong Id { get; } = id; public int MaxPlayers { get; } public double GameClock = 999999999; + public AssetDatabase? Assets { get; } = assetDatabase; + public ChainData Chain { get; } = new(); + private int PlayersConnected = 0; private bool ReadyForStart = false; + private uint _nextObjectId = 1; + private readonly Dictionary _playerCharacterObjectIds = new(); public GameState State { get; private set; } = GameState.Initializing; public IEnumerable GetPlayerIds() => Players.Where(p => !p.Value.IsBot).Select(p => p.Value.Id); @@ -31,21 +40,19 @@ public void Update() { case GameState.Initializing: break; - case GameState.InGame: + case GameState.PreDungeon: + break; + case GameState.Dungeon: break; } } public bool SetupPlayer(ulong playerId, byte slot) { - if (Players.TryGetValue(slot, out var player)) - return player.Id == playerId; - - if (playerId > 10) - Players.Add(slot, new Player(playerId, slot)); - else - Bots.Add(slot, new Bot(playerId, slot)); + if (ExpectedPlayers.ContainsKey(playerId)) + return true; + ExpectedPlayers.Add(playerId, slot); return true; } @@ -54,7 +61,7 @@ public void SetupBot(byte slot) Bots.Add(slot, new(0, slot)); } - public bool AttachPlayer(AccountModel account) + public bool AttachPlayer(AccountModel account, RakNetClient client) { if (!ExpectedPlayers.TryGetValue(account.Id, out var slot)) return false; @@ -63,13 +70,17 @@ public bool AttachPlayer(AccountModel account) Clients.Add(account.Id, account); - var player = new Player(account.Id, slot); + var player = new Player(account.Id, slot) + { + Client = client + }; Players.Add(slot, player); OnHelloPlayer(player); + OnPartyMergeComplete(player); OnPlayerJoined(player); - OnPrepareForStart(player); + SendLabsPlayerUpdate(player.Client); return true; } @@ -85,6 +96,11 @@ private void OnHelloPlayer(Player player) player.Client.SendPacket(helloPlayer); } + private void OnPartyMergeComplete(Player player) + { + player.Client.SendPacket(new PartyMergeCompletePacket()); + } + private void OnPlayerJoined(Player joiningPlayer) { var playerJoinedPacket = new PlayerJoinedPacket(joiningPlayer.Slot); @@ -110,32 +126,307 @@ private void OnPlayerJoined(Player joiningPlayer) } } - private void OnPrepareForStart(Player player) + private void SendLabsPlayerUpdate(RakNetClient client) { - var prepareForStart = new GamePrepareForStartPacket(player.Slot) + Player? player = null; + foreach (var p in Players.Values) + { + if (p.Client == client) + { + player = p; + break; + } + } + + if (player == null) return; + + var updatePacket = new LabsPlayerUpdatePacket { - unk2 = 4, - pLevelAsset = 0x946D41FE, - markerSet1 = 0x6B9636C0, - markerSet2 = 0x6B9636C0, - unk6 = 0 + PlayerId = player.Slot, + UpdateBits = LabsPlayerUpdatePacket.PlayerBits | LabsPlayerUpdatePacket.CharacterMask | LabsPlayerUpdatePacket.CrystalMask, + PlayerData = new LabsPlayerData + { + DataSetup = true, + CurrentDeckIndex = 0, + QueuedDeckIndex = 0, + PlayerIndex = player.Slot, + Team = 1, + PlayerOnlineId = player.Id, + Status = 0, + StatusProgress = 0, + CurrentCreatureId = 0, + EnergyPoints = 0, + IsCharged = true, + DNA = 0, + LockCamera = false, + LockedOverdrive = false, + LockedCrystals = false, + LockedAbilityMin = 0xFF, + LockedDeckIndexMin = 0xFF, + DeckScore = 500, + AvatarLevel = 30, + AvatarXP = 0f, + ChainProgression = 10 + } }; - player.Client.SendPacket(prepareForStart); - } + // Add 3 fake characters + for (int i = 0; i < 3; i++) + { + updatePacket.Characters[i] = new LabsCharacterData + { + Version = 1, + NounId = 0x3039C538, // SageBasic + AssetId = 0, + CreatureType = 1, + DeployCooldown = 0, + AbilityPoints = 10, + AbilityRanks = new uint[] { 1, 1, 1, 1, 1, 1, 1, 1, 1 }, + Health = 200f, + MaxHealth = 200f, + Mana = 200f, + MaxMana = 200f, + GearScore = 300f, + GearScoreFlattened = 300f + }; + } + + // Add 9 fake catalysts + for (int i = 0; i < 9; i++) + { + updatePacket.Catalysts[i] = new LabsCatalystData + { + NounId = i < 8 ? 0x02FB89EB : 0u, // Catalyst_Health + Rarity = 2 + }; + } + + client.SendPacket(updatePacket); + Console.WriteLine("[Game] Sent LabsPlayerUpdate"); + } public void HandlePacket(RakNetClient sender, IRakNetPacket packet) { - // switch (packet) - // { - // case PlayerStatusUpdatePacket playerStatusUpdatePacket: - // if (playerStatusUpdatePacket.Status == 0x80) - // { - // sender.SendPacket(new EnterPreGameFlowPacket()); - // GameClock = (int)(DateTime.UtcNow - StartTime).TotalMilliseconds + 25000; - // State = GameState.ShaperSelect; - // } - // break; - // } + switch (packet) + { + case DebugPingPacket: + HandleDebugPing(sender); + break; + + case ChainPlayerMsgsPacket chainPlayerMsgs: + HandleChainPlayerMsgs(sender, chainPlayerMsgs); + break; + + case PlayerStatusUpdatePacket playerStatusUpdate: + HandlePlayerStatusUpdate(sender, playerStatusUpdate); + break; + + case ActionCommandMsgsPacket actionCommand: + HandleActionCommand(sender, actionCommand); + break; + } + } + + private void HandleDebugPing(RakNetClient sender) + { + Console.WriteLine($"[Game] OnDebugPing (State={State})"); + + switch (State) + { + case GameState.Initializing: + State = GameState.ChainVoting; + break; + + case GameState.ChainVoting: + sender.SendPacket(new ChainVoteMsgsPacket { Value = 0, ChainData = Chain }); + sender.SendPacket(new ChainVoteMsgsPacket { Value = 1, SecondsUntilDeployment = 30f }); + break; + + case GameState.PreDungeon: + break; + + case GameState.Dungeon: + sender.SendPacket(new DirectorStatePacket()); + sender.SendPacket(new QuickGameMsgsPacket()); + OnPlayerStart(sender); + break; + } + } + + private void HandleChainPlayerMsgs(RakNetClient sender, ChainPlayerMsgsPacket packet) + { + Console.WriteLine($"[Game] OnChainPlayerMsgs({packet.ByteCount}): {packet.Value}"); + + if (packet.ByteCount == 1) + { + if (packet.Value == 0) + { + if (Assets != null) Chain.PopulateFromLevel(Assets); + sender.SendPacket(new ChainVoteMsgsPacket { Value = 0, ChainData = Chain }); + sender.SendPacket(new ChainVoteMsgsPacket { Value = 1, SecondsUntilDeployment = 30f }); + Console.WriteLine("[Game] Sent ChainVoteMessages"); + } + else if (packet.Value == 2) + { + sender.SendPacket(new ChainVoteMsgsPacket { Value = 2, StayInParty = false }); + } + } + else if (packet.ByteCount == 6) + { + State = GameState.PreDungeon; + + // Apply the actual user level vote index + Chain.SetLevelByIndex((int)packet.LevelIndex); + if (Assets != null) Chain.PopulateFromLevel(Assets); + + var prepareStart = new GamePrepareForStartPacket(Chain.Level, Chain.MarkerSet, 1, Chain.LevelIndex); + sender.SendPacket(prepareStart); + + SendLabsPlayerUpdate(sender); + } + } + + private void HandlePlayerStatusUpdate(RakNetClient sender, PlayerStatusUpdatePacket packet) + { + Console.WriteLine($"[Game] OnPlayerStatusUpdate: {packet.Status} (State={State})"); + + if (packet.Status == 0x08) + { + State = GameState.Dungeon; + sender.SendPacket(new GameStartPacket(0)); + sender.SendPacket(new DebugPingPacket()); + + SendLabsPlayerUpdate(sender); + } + else + { + SendLabsPlayerUpdate(sender); + } + } + + private void OnPlayerStart(RakNetClient client) + { + Player? player = null; + foreach (var p in Players.Values) + { + if (p.Client == client) + { + player = p; + break; + } + } + + if (player == null) return; + + if (Assets != null) + { + var markers = Assets.GetLevelMarkers($"{Chain.LevelName}.level"); + foreach (var marker in markers) + { + var nounDef = marker["nounDef"]?.AsUInt32() ?? 0; + if (nounDef == 0) continue; + + var markerPos = marker["pos"]?.AsVector3() ?? Vector3.Zero; + var markerScale = marker["scale"]?.AsFloat() ?? 1.0f; + + var markerObjId = _nextObjectId++; + var npcPacket = new ObjectCreatePacket + { + ObjectId = markerObjId, + CreateData = new GameObjectCreateData + { + Noun = nounDef, + Position = markerPos, + Scale = markerScale, + Team = 2, + HasCollision = true, + PlayerControlled = false + }, + ObjectData = new SporelabsObject + { + Position = markerPos, + Team = 2, + PlayerControlled = false, + Visible = true, + HasCollision = true, + Scale = markerScale, + MarkerScale = markerScale + } + }; + client.SendPacket(npcPacket); + } + } + + var spawnPos = new Vector3(44.0f, 0.47f, 17.5f); + uint creatureNoun = 0x3039C538; + + var objectId = _nextObjectId++; + _playerCharacterObjectIds[player.Slot] = objectId; + + var createPacket = new ObjectCreatePacket + { + ObjectId = objectId, + CreateData = new GameObjectCreateData + { + Noun = creatureNoun, + Position = spawnPos, + Scale = 1.0f, + Team = 1, + HasCollision = true, + PlayerControlled = true + }, + ObjectData = new SporelabsObject + { + Position = spawnPos, + Team = 1, + PlayerControlled = true, + PlayerIdx = player.Slot, + Visible = true, + HasCollision = true, + Scale = 1.0f, + MarkerScale = 1.0f + } + }; + client.SendPacket(createPacket); + Console.WriteLine($"[Game] Spawned hero objectId={objectId} noun=0x{creatureNoun:X} at ({spawnPos.X},{spawnPos.Y},{spawnPos.Z})"); + + client.SendPacket(new PlayerCharacterDeployPacket(player.Slot, objectId)); + } + + private void HandleActionCommand(RakNetClient sender, ActionCommandMsgsPacket packet) + { + Console.WriteLine($"[Game] ActionCommand: type={packet.CommandType} obj=0x{packet.ObjectId:X} pos=({packet.PosX:F1},{packet.PosY:F1},{packet.PosZ:F1})"); + + if (packet.CommandType == 3) + { + var movePacket = new ObjectPlayerMovePacket + { + ObjectId = packet.ObjectId, + Locomotion = new LocomotionData + { + GoalFlags = 0x001, + GoalPosition = new Vector3(packet.PosX, packet.PosY, packet.PosZ), + AllowedStopDistance = 0, + DesiredStopDistance = 0 + } + }; + sender.SendPacket(movePacket); + } + else if (packet.CommandType == 4) + { + var movePacket = new ObjectPlayerMovePacket + { + ObjectId = packet.ObjectId, + Locomotion = new LocomotionData + { + GoalFlags = 0x020 + } + }; + sender.SendPacket(movePacket); + } + else if (packet.CommandType == 5) + { + Console.WriteLine($"[Game] Switch character requested"); + } } } diff --git a/ReCap.Server/Domain/Gameplay/GameplayState.cs b/ReCap.Server/Domain/Gameplay/GameplayState.cs index 055ce5f..f5e35d9 100644 --- a/ReCap.Server/Domain/Gameplay/GameplayState.cs +++ b/ReCap.Server/Domain/Gameplay/GameplayState.cs @@ -1,10 +1,12 @@ namespace ReCap.Server.Domain.Gameplay; -public enum GameplayState +public enum GameState { - Initialization, - ShaperSelection, - InGame, - Rewards, + Initializing, + Spaceship, + ChainVoting, + PreDungeon, + Dungeon, + ChainCashOut, Finished } \ No newline at end of file diff --git a/ReCap.Server/Domain/Gameplay/Objects/GameObjectCreateData.cs b/ReCap.Server/Domain/Gameplay/Objects/GameObjectCreateData.cs new file mode 100644 index 0000000..32e5ece --- /dev/null +++ b/ReCap.Server/Domain/Gameplay/Objects/GameObjectCreateData.cs @@ -0,0 +1,61 @@ +using System.IO; +using System.Numerics; +using ReCap.Server.Adapters.RakNet; +using ReCap.Server.Util; + +namespace ReCap.Server.Domain.Gameplay.Objects; + +public class GameObjectCreateData +{ + public uint Noun { get; set; } + public Vector3 Position { get; set; } + public float RotXDegrees { get; set; } + public float RotYDegrees { get; set; } + public float RotZDegrees { get; set; } + public ulong AssetId { get; set; } + public float Scale { get; set; } + public byte Team { get; set; } + public bool HasCollision { get; set; } + public bool PlayerControlled { get; set; } + + public void WriteTo(Stream stream) + { + using var writer = new BinaryWriter(stream, System.Text.Encoding.UTF8, true); + var baseOffset = stream.Position; + + stream.Position = baseOffset; + writer.WriteBE(Noun); + writer.WriteBE(Position); + writer.WriteBE(RotXDegrees); + writer.WriteBE(RotYDegrees); + writer.WriteBE(RotZDegrees); + + stream.Position = baseOffset + 0x20; + writer.WriteBE(AssetId); + writer.WriteBE(Scale); + writer.Write(Team); + writer.Write(HasCollision); + writer.Write(PlayerControlled); + + stream.Position = baseOffset + 0x70; + } + + public void WriteReflection(Stream stream) + { + using var writer = new BinaryWriter(stream, System.Text.Encoding.UTF8, true); + var reflector = new ReflectionSerializer(writer, 10); + + reflector.Begin(); + reflector.Write(0, () => writer.WriteBE(Noun)); + reflector.Write(1, () => writer.WriteBE(Position)); + reflector.Write(2, () => writer.WriteBE(RotXDegrees)); + reflector.Write(3, () => writer.WriteBE(RotYDegrees)); + reflector.Write(4, () => writer.WriteBE(RotZDegrees)); + reflector.Write(5, () => writer.WriteBE(AssetId)); + reflector.Write(6, () => writer.WriteBE(Scale)); + reflector.Write(7, () => writer.Write(Team)); + reflector.Write(8, () => writer.Write(HasCollision)); + reflector.Write(9, () => writer.Write(PlayerControlled)); + reflector.End(); + } +} diff --git a/ReCap.Server/Domain/Gameplay/Objects/LocomotionData.cs b/ReCap.Server/Domain/Gameplay/Objects/LocomotionData.cs new file mode 100644 index 0000000..0bf6451 --- /dev/null +++ b/ReCap.Server/Domain/Gameplay/Objects/LocomotionData.cs @@ -0,0 +1,225 @@ +using System.IO; +using System.Numerics; +using ReCap.Server.Adapters.RakNet; +using ReCap.Server.Util; + +namespace ReCap.Server.Domain.Gameplay.Objects; + +public class LobParams +{ + public float PlaneDirLinearParam { get; set; } + public float UpLinearParam { get; set; } + public float UpQuadraticParam { get; set; } + public Vector3 LobUpDir { get; set; } + public Vector3 PlaneDir { get; set; } + public int BounceNum { get; set; } + public float BounceRestitution { get; set; } + public bool GroundCollisionOnly { get; set; } + public bool StopBounceOnCreatures { get; set; } + + public void WriteTo(Stream stream) + { + using var writer = new BinaryWriter(stream, System.Text.Encoding.UTF8, true); + var baseOffset = stream.Position; + + stream.Position = baseOffset + 0x18; + writer.WriteBE(LobUpDir); + + stream.Position = baseOffset + 0x30; + writer.WriteBE(BounceNum); + writer.WriteBE(BounceRestitution); + writer.Write(GroundCollisionOnly); + writer.Write(StopBounceOnCreatures); + + stream.Position = baseOffset + 0x3C; + writer.WriteBE(PlaneDir); + + stream.Position = baseOffset + 0x48; + writer.WriteBE(PlaneDirLinearParam); + writer.WriteBE(UpLinearParam); + writer.WriteBE(UpQuadraticParam); + + stream.Position = baseOffset + 0x54; + } + + public void WriteReflection(Stream stream) + { + using var writer = new BinaryWriter(stream, System.Text.Encoding.UTF8, true); + var reflector = new ReflectionSerializer(writer, 9); + + reflector.Begin(); + reflector.Write(0, () => writer.WriteBE(PlaneDirLinearParam)); + reflector.Write(1, () => writer.WriteBE(UpLinearParam)); + reflector.Write(2, () => writer.WriteBE(UpQuadraticParam)); + reflector.Write(3, () => writer.WriteBE(LobUpDir)); + reflector.Write(4, () => writer.WriteBE(PlaneDir)); + reflector.Write(5, () => writer.WriteBE(BounceNum)); + reflector.Write(6, () => writer.WriteBE(BounceRestitution)); + reflector.Write(7, () => writer.Write(GroundCollisionOnly)); + reflector.Write(8, () => writer.Write(StopBounceOnCreatures)); + reflector.End(); + } +} + +public class ProjectileParams +{ + public float Speed { get; set; } + public float Acceleration { get; set; } + public uint JinkInfo { get; set; } + public float Range { get; set; } + public float SpinRate { get; set; } + public Vector3 Direction { get; set; } + public byte ProjectileFlags { get; set; } + public float HomingDelay { get; set; } + public float TurnRate { get; set; } + public float TurnAcceleration { get; set; } + public float Eccentricity { get; set; } + public bool Piercing { get; set; } + public bool IgnoreGroundCollide { get; set; } + public bool IgnoreCreatureCollide { get; set; } + public float CombatantSweepHeight { get; set; } + + public void WriteTo(Stream stream) + { + using var writer = new BinaryWriter(stream, System.Text.Encoding.UTF8, true); + var baseOffset = stream.Position; + + stream.Position = baseOffset; + writer.WriteBE(Speed); + writer.WriteBE(Acceleration); + writer.WriteBE(JinkInfo); + writer.WriteBE(Range); + writer.WriteBE(SpinRate); + writer.WriteBE(Direction); + writer.Write(ProjectileFlags); + + stream.Position = baseOffset + 0x24; + writer.WriteBE(HomingDelay); + writer.WriteBE(TurnRate); + writer.WriteBE(TurnAcceleration); + writer.Write(Piercing); + writer.Write(IgnoreGroundCollide); + writer.Write(IgnoreCreatureCollide); + + stream.Position = baseOffset + 0x34; + writer.WriteBE(Eccentricity); + writer.WriteBE(CombatantSweepHeight); + + stream.Position = baseOffset + 0x3C; + } + + public void WriteReflection(Stream stream) + { + using var writer = new BinaryWriter(stream, System.Text.Encoding.UTF8, true); + var reflector = new ReflectionSerializer(writer, 15); + + reflector.Begin(); + reflector.Write(0, () => writer.WriteBE(Speed)); + reflector.Write(1, () => writer.WriteBE(Acceleration)); + reflector.Write(2, () => writer.WriteBE(JinkInfo)); + reflector.Write(3, () => writer.WriteBE(Range)); + reflector.Write(4, () => writer.WriteBE(SpinRate)); + reflector.Write(5, () => writer.WriteBE(Direction)); + reflector.Write(6, () => writer.Write(ProjectileFlags)); + reflector.Write(7, () => writer.WriteBE(HomingDelay)); + reflector.Write(8, () => writer.WriteBE(TurnRate)); + reflector.Write(9, () => writer.WriteBE(TurnAcceleration)); + reflector.Write(10, () => writer.WriteBE(Eccentricity)); + reflector.Write(11, () => writer.Write(Piercing)); + reflector.Write(12, () => writer.Write(IgnoreGroundCollide)); + reflector.Write(13, () => writer.Write(IgnoreCreatureCollide)); + reflector.Write(14, () => writer.WriteBE(CombatantSweepHeight)); + reflector.End(); + } +} + +public class LocomotionData +{ + public ulong LobStartTime { get; set; } + public float LobPrevSpeedModifier { get; set; } + public LobParams LobParams { get; set; } = new(); + public ProjectileParams ProjectileParams { get; set; } = new(); + public uint GoalFlags { get; set; } + public Vector3 GoalPosition { get; set; } + public Vector3 PartialGoalPosition { get; set; } + public Vector3 Facing { get; set; } + public Vector3 ExternalLinearVelocity { get; set; } + public Vector3 ExternalForce { get; set; } + public float AllowedStopDistance { get; set; } + public float DesiredStopDistance { get; set; } + public uint TargetObjectId { get; set; } + public Vector3 TargetPosition { get; set; } + public Vector3 ExpectedGeoCollision { get; set; } + public Vector3 InitialDirection { get; set; } + public Vector3 Offset { get; set; } + public int ReflectedLastUpdate { get; set; } + + public void WriteTo(Stream stream) + { + using var writer = new BinaryWriter(stream, System.Text.Encoding.UTF8, true); + var baseOffset = stream.Position; + + stream.Position = baseOffset + 0x08; + writer.WriteBE(ReflectedLastUpdate); + + stream.Position = baseOffset + 0x44; + ProjectileParams.WriteTo(stream); + + stream.Position = baseOffset + 0x84; + writer.WriteBE(ExpectedGeoCollision); + writer.WriteBE(TargetObjectId); + + stream.Position = baseOffset + 0x9C; + writer.WriteBE(InitialDirection); + + stream.Position = baseOffset + 0xD8; + writer.WriteBE(LobStartTime); + writer.WriteBE(LobPrevSpeedModifier); + LobParams.WriteTo(stream); + + stream.Position = baseOffset + 0x138; + writer.WriteBE(Offset); + writer.WriteBE(GoalFlags); + writer.WriteBE(GoalPosition); + writer.WriteBE(PartialGoalPosition); + + stream.Position = baseOffset + 0x178; + writer.WriteBE(Facing); + writer.WriteBE(ExternalLinearVelocity); + writer.WriteBE(ExternalForce); + writer.WriteBE(AllowedStopDistance); + writer.WriteBE(DesiredStopDistance); + + stream.Position = baseOffset + 0x1AC; + writer.WriteBE(TargetPosition); + + stream.Position = baseOffset + 0x290; + } + + public void WriteReflection(Stream stream) + { + using var writer = new BinaryWriter(stream, System.Text.Encoding.UTF8, true); + var reflector = new ReflectionSerializer(writer, 18); + + reflector.Begin(); + reflector.Write(0, () => writer.WriteBE(LobStartTime)); + reflector.Write(1, () => writer.WriteBE(LobPrevSpeedModifier)); + reflector.Write(2, () => LobParams.WriteReflection(stream)); + reflector.Write(3, () => ProjectileParams.WriteReflection(stream)); + reflector.Write(4, () => writer.WriteBE(GoalFlags)); + reflector.Write(5, () => writer.WriteBE(GoalPosition)); + reflector.Write(6, () => writer.WriteBE(PartialGoalPosition)); + reflector.Write(7, () => writer.WriteBE(Facing)); + reflector.Write(8, () => writer.WriteBE(ExternalLinearVelocity)); + reflector.Write(9, () => writer.WriteBE(ExternalForce)); + reflector.Write(10, () => writer.WriteBE(AllowedStopDistance)); + reflector.Write(11, () => writer.WriteBE(DesiredStopDistance)); + reflector.Write(12, () => writer.WriteBE(TargetObjectId)); + reflector.Write(13, () => writer.WriteBE(TargetPosition)); + reflector.Write(14, () => writer.WriteBE(ExpectedGeoCollision)); + reflector.Write(15, () => writer.WriteBE(InitialDirection)); + reflector.Write(16, () => writer.WriteBE(Offset)); + reflector.Write(17, () => writer.WriteBE(ReflectedLastUpdate)); + reflector.End(); + } +} diff --git a/ReCap.Server/Domain/Gameplay/Objects/SporelabsObject.cs b/ReCap.Server/Domain/Gameplay/Objects/SporelabsObject.cs new file mode 100644 index 0000000..a78b11e --- /dev/null +++ b/ReCap.Server/Domain/Gameplay/Objects/SporelabsObject.cs @@ -0,0 +1,120 @@ +using System.IO; +using System.Numerics; +using ReCap.Server.Adapters.RakNet; +using ReCap.Server.Util; + +namespace ReCap.Server.Domain.Gameplay.Objects; + +public class SporelabsObject +{ + public byte Team { get; set; } = 1; + public bool PlayerControlled { get; set; } + public uint InputSyncStamp { get; set; } + public byte PlayerIdx { get; set; } + public Vector3 LinearVelocity { get; set; } + public Vector3 AngularVelocity { get; set; } + public Vector3 Position { get; set; } + public Quaternion Orientation { get; set; } = Quaternion.Identity; + public float Scale { get; set; } = 1f; + public float MarkerScale { get; set; } = 1f; + public uint LastAnimationState { get; set; } + public ulong LastAnimationPlayTimeMs { get; set; } + public uint OverrideMoveIdleAnimationState { get; set; } + public uint GraphicsState { get; set; } + public ulong GraphicsStateStartTimeMs { get; set; } + public ulong NewGraphicsStateStartTimeMs { get; set; } + public bool Visible { get; set; } = true; + public bool HasCollision { get; set; } + public uint OwnerID { get; set; } + public byte MovementType { get; set; } + public bool DisableRepulsion { get; set; } + public uint InteractableState { get; set; } + public uint SourceMarkerKeyMarkerId { get; set; } + + public void WriteTo(Stream stream) + { + using var writer = new BinaryWriter(stream, System.Text.Encoding.UTF8, true); + var baseOffset = stream.Position; + + stream.Position = baseOffset + 0x010; + writer.WriteBE(Scale); + writer.WriteBE(MarkerScale); + + stream.Position = baseOffset + 0x018; + writer.WriteBE(Position); + writer.WriteBE(Orientation); + writer.WriteBE(LinearVelocity); + writer.WriteBE(AngularVelocity); + + stream.Position = baseOffset + 0x050; + writer.WriteBE(OwnerID); + writer.Write(Team); + writer.Write(PlayerIdx); + + stream.Position = baseOffset + 0x058; + writer.WriteBE(InputSyncStamp); + writer.Write(PlayerControlled); + + stream.Position = baseOffset + 0x05F; + writer.Write(Visible); + writer.Write(HasCollision); + writer.Write(MovementType); + + stream.Position = baseOffset + 0x088; + writer.WriteBE(SourceMarkerKeyMarkerId); + + stream.Position = baseOffset + 0x0AC; + writer.WriteBE(LastAnimationState); + + stream.Position = baseOffset + 0x0B8; + writer.WriteBE(LastAnimationPlayTimeMs); + writer.WriteBE(OverrideMoveIdleAnimationState); + + stream.Position = baseOffset + 0x258; + writer.WriteBE(GraphicsState); + + stream.Position = baseOffset + 0x260; + writer.WriteBE(GraphicsStateStartTimeMs); + writer.WriteBE(NewGraphicsStateStartTimeMs); + + stream.Position = baseOffset + 0x284; + writer.Write(DisableRepulsion); + + stream.Position = baseOffset + 0x288; + writer.WriteBE(InteractableState); + + stream.Position = baseOffset + 0x308; + } + + public void WriteReflection(Stream stream) + { + using var writer = new BinaryWriter(stream, System.Text.Encoding.UTF8, true); + var reflector = new ReflectionSerializer(writer, 23); + + reflector.Begin(); + reflector.Write(0, () => writer.Write(Team)); + reflector.Write(1, () => writer.Write(PlayerControlled)); + reflector.Write(2, () => writer.WriteBE(InputSyncStamp)); + reflector.Write(3, () => writer.Write(PlayerIdx)); + reflector.Write(4, () => writer.WriteBE(LinearVelocity)); + reflector.Write(5, () => writer.WriteBE(AngularVelocity)); + reflector.Write(6, () => writer.WriteBE(Position)); + reflector.Write(7, () => writer.WriteBE(Orientation)); + reflector.Write(8, () => writer.WriteBE(Scale)); + reflector.Write(9, () => writer.WriteBE(MarkerScale)); + reflector.Write(10, () => writer.WriteBE(LastAnimationState)); + reflector.Write(11, () => writer.WriteBE(LastAnimationPlayTimeMs)); + reflector.Write(12, () => writer.WriteBE(OverrideMoveIdleAnimationState)); + reflector.Write(13, () => writer.WriteBE(GraphicsState)); + reflector.Write(14, () => writer.WriteBE(GraphicsStateStartTimeMs)); + reflector.Write(15, () => writer.WriteBE(NewGraphicsStateStartTimeMs)); + reflector.Write(16, () => writer.Write(Visible)); + reflector.Write(17, () => writer.Write(HasCollision)); + reflector.Write(18, () => writer.WriteBE(OwnerID)); + reflector.Write(19, () => writer.Write(MovementType)); + reflector.Write(20, () => writer.Write(DisableRepulsion)); + reflector.Write(21, () => writer.WriteBE(InteractableState)); + reflector.Write(22, () => writer.WriteBE(SourceMarkerKeyMarkerId)); + reflector.End(); + } +} diff --git a/ReCap.Server/Program.cs b/ReCap.Server/Program.cs index fcd2c71..5418494 100644 --- a/ReCap.Server/Program.cs +++ b/ReCap.Server/Program.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using System.Linq; using System.Net; @@ -8,6 +8,7 @@ using ReCap.Server.Adapters.RakNet; using ReCap.Server.Adapters.Rest.Api; using ReCap.Server.Config; +using ReCap.Server.Services; using ReCap.Server.Util; namespace ReCap.Server; @@ -22,10 +23,12 @@ public static class Program const string _HELP_ARG = "--help"; const string _PORT_ARG = "--port="; const string _DB_PATH_ARG = "--database-path="; + const string _GAME_PATH_ARG = "--game-path="; static async Task Main(string[] args) { #nullable disable string databasePath = null; + string gamePath = null; int port = Api.DEFAULT_PORT; @@ -57,6 +60,16 @@ static async Task Main(string[] args) databasePath = dbPath; } } + else if (arg.StartsWith(_GAME_PATH_ARG)) + { + string gPath = arg.Substring(_GAME_PATH_ARG.Length); + gPath = CommandLineHelper.UnwrapArg(gPath); + + if (File.Exists(gPath)) + gamePath = gPath; + else + Logger.error($"Game path not found: '{gPath}'"); + } } @@ -85,8 +98,20 @@ static async Task Main(string[] args) serverOpts.ServerDatabaseDirectory = databasePath; } + if (!string.IsNullOrWhiteSpace(gamePath)) + { + Logger.info($"Using game path: '{gamePath}'"); + serverOpts.GamePath = gamePath; + } + ServerConfig.Configure(serverOpts); + var assetDatabase = string.IsNullOrWhiteSpace(ServerConfig.GamePath) + ? null + : new AssetDatabase(ServerConfig.GamePath); + + var gameService = new GameService { Assets = assetDatabase }; + CancellationTokenSource source = new CancellationTokenSource(); CancellationToken token = source.Token; @@ -102,11 +127,11 @@ static async Task Main(string[] args) Task.Run(redirector.Start); - BlazeServer lobby = new(dbConfig, "Lobby", localhostIP, 42125, false, hostname); + BlazeServer lobby = new(dbConfig, "Lobby", localhostIP, 42125, false, hostname, gameService); Task.Run(lobby.Start); - RakNetServer raknet = new(dbConfig, "RakNet", localhostIP, 42000, false, hostname); + RakNetServer raknet = new(dbConfig, "RakNet", localhostIP, 42000, false, hostname, assetDatabase, gameService); Task.Run(() => raknet.ExecuteAsync(token)); #pragma warning restore CS4014 @@ -171,6 +196,7 @@ static async Task TryRelaunchElevatedAsync(bool returnIfAlreadyElevated = string.Empty, $"{_BEFORE_ARG}{_PORT_ARG} {_AFTER_ARG}Port number", $"{_BEFORE_ARG}{_DB_PATH_ARG}{_AFTER_ARG}Path to a directory in which to create/store/access the 'server.db'", + $"{_BEFORE_ARG}{_GAME_PATH_ARG}{_AFTER_ARG}Path to the Darkspore AssetData_Binary.package file", }.AsReadOnly(); static void PrintHelp() { diff --git a/ReCap.Server/ReCap.Server.csproj b/ReCap.Server/ReCap.Server.csproj index 0e20954..cf3472a 100755 --- a/ReCap.Server/ReCap.Server.csproj +++ b/ReCap.Server/ReCap.Server.csproj @@ -1,14 +1,15 @@ - + Exe - net8.0 + net9.0 enable enable + diff --git a/ReCap.Server/ReCap.Server.sln b/ReCap.Server/ReCap.Server.sln new file mode 100644 index 0000000..5f4ab8b --- /dev/null +++ b/ReCap.Server/ReCap.Server.sln @@ -0,0 +1,24 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.2.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReCap.Server", "ReCap.Server.csproj", "{E85A17E1-4733-F9F1-C9FC-BBB62D1C44E1}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {E85A17E1-4733-F9F1-C9FC-BBB62D1C44E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E85A17E1-4733-F9F1-C9FC-BBB62D1C44E1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E85A17E1-4733-F9F1-C9FC-BBB62D1C44E1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E85A17E1-4733-F9F1-C9FC-BBB62D1C44E1}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {0905D019-6F3F-486C-8A61-1B99D8487A3E} + EndGlobalSection +EndGlobal diff --git a/ReCap.Server/Services/AssetDatabase.cs b/ReCap.Server/Services/AssetDatabase.cs index 102f248..a3b7378 100644 --- a/ReCap.Server/Services/AssetDatabase.cs +++ b/ReCap.Server/Services/AssetDatabase.cs @@ -35,6 +35,64 @@ public AssetDatabase(string packagePath) return node; } + public AssetNode? GetAssetById(ulong assetId, string extension) + { + uint instanceId = (uint)assetId; + uint groupId = (uint)(assetId >> 32); + uint typeHash = DbpfReader.FnvHash(extension); + + foreach (var entry in _reader.Entries) + { + if (entry.Key.InstanceId == instanceId && entry.Key.TypeId == typeHash) + { + // Optionally check groupId if it's non-zero + if (groupId != 0 && entry.Key.GroupId != groupId) continue; + + string cacheKey = $"0x{assetId:X16}.{extension}"; + if (_cache.TryGetValue(cacheKey, out var cached)) + return cached; + + var data = _reader.ReadEntry(entry); + if (data == null) return null; + + var fileType = _parser.GetFileType(extension); + if (fileType == null) return null; + + var node = _parser.Parse(data, fileType.RootStruct, fileType.HeaderSize); + _cache[cacheKey] = node; + return node; + } + } + + return null; + } + + public IEnumerable GetLevelMarkers(string levelName) + { + var levelNode = GetAsset(levelName); + if (levelNode == null) yield break; + + var markersets = levelNode["markersets"]?.Elements; + if (markersets == null) yield break; + + foreach (var markersetRef in markersets) + { + var assetId = markersetRef["markersetAsset"]?.AsUInt64() ?? 0; + if (assetId == 0) continue; + + var markersetNode = GetAssetById(assetId, "markerset"); + if (markersetNode == null) continue; + + var markers = markersetNode["markers"]?.Elements; + if (markers == null) continue; + + foreach (var marker in markers) + { + yield return marker; + } + } + } + public IEnumerable ListAssets() => _reader.ListAssets(); public void Dispose() => _reader.Dispose(); diff --git a/ReCap.Server/Util/BigEndianExtensions.cs b/ReCap.Server/Util/BigEndianExtensions.cs new file mode 100644 index 0000000..846a9ac --- /dev/null +++ b/ReCap.Server/Util/BigEndianExtensions.cs @@ -0,0 +1,89 @@ +using System; +using System.Buffers.Binary; +using System.IO; +using System.Numerics; +using System.Text; + +namespace ReCap.Server.Util; + +public static class BigEndianExtensions +{ + public static uint ReadUInt32BE(this BinaryReader reader) + { + return BinaryPrimitives.ReverseEndianness(reader.ReadUInt32()); + } + + public static float ReadSingleBE(this BinaryReader reader) + { + uint val = reader.ReadUInt32(); + uint swapped = BinaryPrimitives.ReverseEndianness(val); + return BitConverter.UInt32BitsToSingle(swapped); + } + + public static ushort ReadUInt16BE(this BinaryReader reader) + { + return BinaryPrimitives.ReverseEndianness(reader.ReadUInt16()); + } + + public static ulong ReadUInt64BE(this BinaryReader reader) + { + return BinaryPrimitives.ReverseEndianness(reader.ReadUInt64()); + } + + public static void WriteBE(this BinaryWriter writer, uint value) + { + writer.Write(BinaryPrimitives.ReverseEndianness(value)); + } + + public static void WriteBE(this BinaryWriter writer, ushort value) + { + writer.Write(BinaryPrimitives.ReverseEndianness(value)); + } + + public static void WriteBE(this BinaryWriter writer, ulong value) + { + writer.Write(BinaryPrimitives.ReverseEndianness(value)); + } + + public static void WriteBE(this BinaryWriter writer, int value) + { + writer.Write(BinaryPrimitives.ReverseEndianness(value)); + } + + public static void WriteBE(this BinaryWriter writer, byte value) + { + writer.Write(value); + } + + public static void WriteBE(this BinaryWriter writer, bool value) + { + writer.Write(value); + } + + public static void WriteBE(this BinaryWriter writer, float value) + { + uint bits = BitConverter.SingleToUInt32Bits(value); + writer.Write(BinaryPrimitives.ReverseEndianness(bits)); + } + + public static void WriteBE(this BinaryWriter writer, Vector2 value) + { + writer.WriteBE(value.X); + writer.WriteBE(value.Y); + } + + public static void WriteBE(this BinaryWriter writer, Vector3 value) + { + writer.WriteBE(value.X); + writer.WriteBE(value.Y); + writer.WriteBE(value.Z); + } + + public static void WriteBE(this BinaryWriter writer, Quaternion value) + { + writer.WriteBE(value.X); + writer.WriteBE(value.Y); + writer.WriteBE(value.Z); + writer.WriteBE(value.W); + } +} From 699a1087694a2b744d9d51444a697b32f63578b8 Mon Sep 17 00:00:00 2001 From: JeanxPereira Date: Sat, 21 Mar 2026 16:25:09 -0300 Subject: [PATCH 6/6] ci: fix runtime identifier build errors in Actions --- .github/workflows/build-launcher.yml | 2 +- .github/workflows/build-server.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-launcher.yml b/.github/workflows/build-launcher.yml index 34bb85d..f37b689 100644 --- a/.github/workflows/build-launcher.yml +++ b/.github/workflows/build-launcher.yml @@ -34,7 +34,7 @@ jobs: - name: Build launcher run: | cd ReCap.Launcher - dotnet build --configuration ${{env.DOTNET_CONFIG}} --runtime ${{matrix.runtime}} + dotnet build ReCap.Launcher.csproj --configuration ${{env.DOTNET_CONFIG}} --runtime ${{matrix.runtime}} - name: Upload artifacts uses: actions/upload-artifact@v4 diff --git a/.github/workflows/build-server.yml b/.github/workflows/build-server.yml index b191c55..806358e 100644 --- a/.github/workflows/build-server.yml +++ b/.github/workflows/build-server.yml @@ -36,7 +36,7 @@ jobs: - name: Build server run: | cd ReCap.Server - dotnet build --configuration ${{env.DOTNET_CONFIG}} --runtime ${{matrix.runtime}} + dotnet build ReCap.Server.csproj --configuration ${{env.DOTNET_CONFIG}} --runtime ${{matrix.runtime}} - name: Upload artifacts uses: actions/upload-artifact@v4