├── readme-screenshot.png ├── src ├── PFire.Console │ ├── uninstall-service.bat │ ├── AssemblyInfo.cs │ ├── install-service.bat │ ├── Properties │ │ └── launchSettings.json │ ├── appsettings.json │ ├── Services │ │ └── PFireServerService.cs │ ├── Extensions │ │ └── ServiceCollectionExtensions.cs │ ├── Program.cs │ └── PFire.Console.csproj ├── PFire.Common │ ├── AssemblyInfo.cs │ ├── Services │ │ └── DateTimeService.cs │ ├── Extensions │ │ └── ServiceCollectionExtensions.cs │ └── PFire.Common.csproj ├── PFire.Core │ ├── AssemblyInfo.cs │ ├── Models │ │ ├── GroupModel.cs │ │ ├── FriendRequestModel.cs │ │ ├── GameModel.cs │ │ ├── User.cs │ │ └── ClientPreferencesModel.cs │ ├── Protocol │ │ ├── Messages │ │ │ ├── Inbound │ │ │ │ ├── Unknown37.cs │ │ │ │ ├── Logout.cs │ │ │ │ ├── GroupRemove.cs │ │ │ │ ├── GameServerFetchAll.cs │ │ │ │ ├── GroupRename.cs │ │ │ │ ├── GameServerFetchFriendsFavorites.cs │ │ │ │ ├── KeepAlive.cs │ │ │ │ ├── GroupCreate.cs │ │ │ │ ├── ClientConfiguration.cs │ │ │ │ ├── UserLookup.cs │ │ │ │ ├── ClientVersion.cs │ │ │ │ ├── FriendRequestDecline.cs │ │ │ │ ├── FriendRemoval.cs │ │ │ │ ├── StatusChange.cs │ │ │ │ ├── FriendRequest.cs │ │ │ │ ├── NicknameChange.cs │ │ │ │ ├── GameInformation.cs │ │ │ │ ├── LoginRequest.cs │ │ │ │ ├── FriendRequestAccept.cs │ │ │ │ ├── ConnectionInformation.cs │ │ │ │ └── ClientPreferencesUpdate.cs │ │ │ ├── Bidirectional │ │ │ │ ├── ChatAcknowledgement.cs │ │ │ │ ├── ChatContent.cs │ │ │ │ ├── ChatTypingNotification.cs │ │ │ │ └── ChatMessage.cs │ │ │ ├── MessageEnums │ │ │ │ └── ChatMessageType.cs │ │ │ ├── IMessage.cs │ │ │ ├── UnknownMessageTypeException.cs │ │ │ ├── Outbound │ │ │ │ ├── LoginFailure.cs │ │ │ │ ├── ChatRooms.cs │ │ │ │ ├── FriendRemoved.cs │ │ │ │ ├── GroupsFriends.cs │ │ │ │ ├── LoginChallenge.cs │ │ │ │ ├── FriendStatusChange.cs │ │ │ │ ├── ServerList.cs │ │ │ │ ├── SystemBroadcastMessage.cs │ │ │ │ ├── ServerPong.cs │ │ │ │ ├── GroupCreateConfirmation.cs │ │ │ │ ├── FriendInvite.cs │ │ │ │ ├── Groups.cs │ │ │ │ ├── Did.cs │ │ │ │ ├── FriendsList.cs │ │ │ │ ├── GameServerSendAll.cs │ │ │ │ ├── FriendsGameInfo.cs │ │ │ │ ├── UserLookupResult.cs │ │ │ │ ├── FriendsSessionAssign.cs │ │ │ │ ├── GameServerSendFriendsFavorites.cs │ │ │ │ ├── LoginSuccess.cs │ │ │ │ └── ClientPreferences.cs │ │ │ ├── XFireMessage.cs │ │ │ └── XFireMessageType.cs │ │ ├── XFireAttributes │ │ │ ├── Int8KeyMapAttribute.cs │ │ │ ├── StringKeyMapAttribute.cs │ │ │ ├── UnknownXFireAttributeException.cs │ │ │ ├── NullAttribute.cs │ │ │ ├── Int32Attribute.cs │ │ │ ├── Int8Attribute.cs │ │ │ ├── SessionIdAttribute.cs │ │ │ ├── DidAttribute.cs │ │ │ ├── StringAttribute.cs │ │ │ ├── XFireAttribute.cs │ │ │ ├── ListAttribute.cs │ │ │ ├── MessageAttribute.cs │ │ │ └── MapAttribute.cs │ │ ├── XMessageField.cs │ │ ├── XFireAttributeFactory.cs │ │ ├── XFireMessageTypeFactory.cs │ │ └── MessageSerializer.cs │ ├── PFire.Core.csproj │ ├── Util │ │ ├── ByteHelpers.cs │ │ ├── LoggerExtensions.cs │ │ └── Disposable.cs │ ├── ITcpServer.cs │ ├── Extensions │ │ └── ServiceCollectionExtensions.cs │ ├── Enums │ │ └── ClientPreferences.cs │ ├── TcpServer.cs │ ├── Session │ │ └── XFireClientManager.cs │ └── PFireServer.cs └── PFire.Infrastructure │ ├── AssemblyInfo.cs │ ├── PFire.Infrastructure.csproj │ ├── Services │ ├── DatabaseContextFactory.cs │ └── DatabaseContext.cs │ ├── Entities │ ├── Group.cs │ ├── Entity.cs │ ├── User.cs │ ├── ClientPreferences.cs │ └── Friend.cs │ ├── Extensions │ └── ServiceCollectionExtensions.cs │ └── Migrations │ ├── 20250120173254_CreateGroup.cs │ ├── 20250202021012_ModifyAuditEntities.cs │ ├── 20250307032454_ClientPreferences.cs │ ├── 20201213224005_InitialMigration.cs │ ├── 20201213224005_InitialMigration.Designer.cs │ ├── 20201219103931_FixTableNames.Designer.cs │ ├── 20250120173254_CreateGroup.Designer.cs │ ├── 20250202021012_ModifyAuditEntities.Designer.cs │ ├── 20201219103931_FixTableNames.cs │ ├── DatabaseContextModelSnapshot.cs │ └── 20250307032454_ClientPreferences.Designer.cs ├── tests └── PFire.Tests │ ├── PFire.Infrastructure │ └── Class1.cs │ ├── BaseTest.cs │ ├── PFire.Tests.csproj │ ├── PFire.Core │ └── Protocol │ │ └── MessageSerializerTests.cs │ └── PFire.Console │ └── Services │ └── PFireServerServiceTests.cs ├── Dockerfile ├── .github └── workflows │ └── dotnet-core.yml ├── LICENSE ├── README.md ├── .gitattributes ├── PFire.sln ├── .editorconfig └── .gitignore /readme-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darcymiranda/PFire/HEAD/readme-screenshot.png -------------------------------------------------------------------------------- /src/PFire.Console/uninstall-service.bat: -------------------------------------------------------------------------------- 1 | sc.exe stop "PFireServer" 2 | sc.exe delete "PFireServer" -------------------------------------------------------------------------------- /tests/PFire.Tests/PFire.Infrastructure/Class1.cs: -------------------------------------------------------------------------------- 1 | namespace PFire.Tests.PFire.Infrastructure 2 | { 3 | public class Class1 4 | { 5 | //placeholder 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/PFire.Common/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("PFire.Tests")] 4 | [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] 5 | -------------------------------------------------------------------------------- /src/PFire.Core/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("PFire.Tests")] 4 | [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] 5 | -------------------------------------------------------------------------------- /src/PFire.Console/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("PFire.Tests")] 4 | [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] 5 | -------------------------------------------------------------------------------- /src/PFire.Infrastructure/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("PFire.Tests")] 4 | [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] 5 | -------------------------------------------------------------------------------- /src/PFire.Console/install-service.bat: -------------------------------------------------------------------------------- 1 | set ABS_PATH=%CD% 2 | sc.exe create "PFireServer" binpath="%CD%\PFire.WindowsService.exe" displayname="PFire Server" 3 | sc.exe description "PFireServer" "Emulated XFire Server" -------------------------------------------------------------------------------- /src/PFire.Core/Models/GroupModel.cs: -------------------------------------------------------------------------------- 1 | namespace PFire.Core.Models 2 | { 3 | internal class GroupModel 4 | { 5 | public int Id { get; set; } 6 | public string Name { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/PFire.Console/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "PFire.Console": { 4 | "commandName": "Project", 5 | "environmentVariables": { 6 | "DOTNET_ENVIRONMENT": "Local" 7 | } 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/Messages/Inbound/Unknown37.cs: -------------------------------------------------------------------------------- 1 | namespace PFire.Core.Protocol.Messages.Inbound 2 | { 3 | internal sealed class Unknown37 : XFireMessage 4 | { 5 | public Unknown37() : base(XFireMessageType.Unknown37) {} 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/XFireAttributes/Int8KeyMapAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace PFire.Core.Protocol.XFireAttributes 2 | { 3 | public class Int8KeyMapAttribute : MapAttribute 4 | { 5 | public override byte AttributeTypeId => 0x09; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/XFireAttributes/StringKeyMapAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace PFire.Core.Protocol.XFireAttributes 2 | { 3 | public class StringKeyMapAttribute : MapAttribute 4 | { 5 | public override byte AttributeTypeId => 0x5; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build 2 | WORKDIR /app 3 | COPY . ./ 4 | RUN dotnet publish -c Release -o out 5 | 6 | FROM mcr.microsoft.com/dotnet/aspnet:8.0 7 | WORKDIR /app 8 | COPY --from=build /app/out . 9 | 10 | EXPOSE 25999 11 | ENTRYPOINT ["dotnet", "PFire.Console.dll"] -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/Messages/Bidirectional/ChatAcknowledgement.cs: -------------------------------------------------------------------------------- 1 | namespace PFire.Core.Protocol.Messages.Bidirectional 2 | { 3 | internal sealed class ChatAcknowledgement : XFireMessage 4 | { 5 | public ChatAcknowledgement() : base(XFireMessageType.ServerChatMessage) {} 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/Messages/MessageEnums/ChatMessageType.cs: -------------------------------------------------------------------------------- 1 | namespace PFire.Core.Protocol.Messages.MessageEnums 2 | { 3 | internal enum ChatMessageType : byte 4 | { 5 | Content = 0, 6 | Acknowledgement, 7 | ClientInformation, 8 | TypingNotification 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/Messages/IMessage.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using PFire.Core.Session; 3 | 4 | namespace PFire.Core.Protocol.Messages 5 | { 6 | internal interface IMessage 7 | { 8 | XFireMessageType MessageTypeId { get; } 9 | Task Process(IXFireClient client); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/PFire.Core/Models/FriendRequestModel.cs: -------------------------------------------------------------------------------- 1 | namespace PFire.Core.Models 2 | { 3 | public class FriendRequestModel 4 | { 5 | public int Id { get; set; } 6 | public string Username { get; set; } 7 | public string Nickname { get; set; } 8 | public string Message { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/Messages/UnknownMessageTypeException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PFire.Core.Protocol.Messages 4 | { 5 | public sealed class UnknownMessageTypeException : Exception 6 | { 7 | public UnknownMessageTypeException(XFireMessageType messageType) : base($"Unknown message type: {messageType}") {} 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/Messages/Outbound/LoginFailure.cs: -------------------------------------------------------------------------------- 1 | namespace PFire.Core.Protocol.Messages.Outbound 2 | { 3 | internal sealed class LoginFailure : XFireMessage 4 | { 5 | public LoginFailure() : base(XFireMessageType.LoginFailure) {} 6 | 7 | [XMessageField("reason")] 8 | public int Reason { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/PFire.Common/Services/DateTimeService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PFire.Common.Services 4 | { 5 | public interface IDateTimeService 6 | { 7 | DateTimeOffset Now { get; } 8 | } 9 | 10 | internal class DateTimeService : IDateTimeService 11 | { 12 | public DateTimeOffset Now => DateTimeOffset.UtcNow; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/XFireAttributes/UnknownXFireAttributeException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PFire.Core.Protocol.XFireAttributes 4 | { 5 | public class UnknownXFireAttributeTypeException : Exception 6 | { 7 | public UnknownXFireAttributeTypeException(byte attributeTypeId) : base($"Unknown xfire attribute type {attributeTypeId}") { } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/PFire.Core/Models/GameModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace PFire.Core.Models 8 | { 9 | public class GameModel 10 | { 11 | public int Id { get; set; } 12 | public int Ip { get; set; } 13 | public int Port { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/PFire.Core/PFire.Core.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net10.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/Messages/Outbound/ChatRooms.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace PFire.Core.Protocol.Messages.Outbound 4 | { 5 | internal sealed class ChatRooms : XFireMessage 6 | { 7 | public ChatRooms() : base(XFireMessageType.ChatRooms) 8 | { 9 | ChatIds = new List(); 10 | } 11 | 12 | [XMessageField(0x04)] 13 | public List ChatIds { get; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/Messages/Bidirectional/ChatContent.cs: -------------------------------------------------------------------------------- 1 | namespace PFire.Core.Protocol.Messages.Bidirectional 2 | { 3 | internal sealed class ChatContent : XFireMessage 4 | { 5 | public ChatContent() : base(XFireMessageType.ServerChatMessage) {} 6 | 7 | [XMessageField("imindex")] 8 | public int MessageOrderIndex { get; set; } 9 | 10 | [XMessageField("im")] 11 | public string MessageContent { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/PFire.Core/Models/User.cs: -------------------------------------------------------------------------------- 1 | namespace PFire.Core.Models 2 | { 3 | public class UserModel 4 | { 5 | public int Id { get; set; } 6 | public string Username { get; set; } 7 | public string Password { get; set; } 8 | public string Nickname { get; set; } 9 | public GameModel Game { get; set; } = new GameModel(); 10 | public ClientPreferencesModel ClientPreferences { get; set; } = new ClientPreferencesModel(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/Messages/Inbound/Logout.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using PFire.Core.Session; 3 | 4 | namespace PFire.Core.Protocol.Messages.Inbound 5 | { 6 | internal sealed class Logout : XFireMessage 7 | { 8 | public Logout() : base(XFireMessageType.Logout) {} 9 | 10 | public override Task Process(IXFireClient client) 11 | { 12 | client.EndSession(); 13 | return Task.CompletedTask; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/PFire.Core/Util/ByteHelpers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PFire.Core.Util 4 | { 5 | public static class ByteHelper 6 | { 7 | public static byte[] CombineByteArray(byte[] a1, byte[] a2) 8 | { 9 | var combined = new byte[a1.Length + a2.Length]; 10 | Buffer.BlockCopy(a1, 0, combined, 0, a1.Length); 11 | Buffer.BlockCopy(a2, 0, combined, a1.Length, a2.Length); 12 | return combined; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/PFire.Common/Extensions/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using PFire.Common.Services; 3 | 4 | namespace PFire.Common.Extensions 5 | { 6 | public static class ServiceCollectionExtensions 7 | { 8 | public static IServiceCollection RegisterCommon(this IServiceCollection serviceCollection) 9 | { 10 | return serviceCollection.AddSingleton(); 11 | } 12 | 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/Messages/Outbound/FriendRemoved.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using PFire.Core.Session; 3 | 4 | namespace PFire.Core.Protocol.Messages.Outbound 5 | { 6 | internal sealed class FriendRemoved : XFireMessage 7 | { 8 | public FriendRemoved(int userId) : base(XFireMessageType.FriendRemoved) 9 | { 10 | UserId = userId; 11 | } 12 | 13 | [XMessageField("userid")] 14 | public int UserId { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/PFire.Console/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "PFire": "Data Source=pfiredb.sqlite" 4 | }, 5 | "Serilog": { 6 | "MinimumLevel": "Debug", 7 | "WriteTo": [ 8 | { 9 | "Name": "Console" 10 | }, 11 | { 12 | "Name": "File", 13 | "Args": { 14 | "path": "Logs/log-.txt", 15 | "rollingInterval": "Day" 16 | } 17 | } 18 | ], 19 | "Properties": { 20 | "Application": "PFire" 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /src/PFire.Common/PFire.Common.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net10.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/PFire.Tests/BaseTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Moq.AutoMock; 3 | 4 | namespace PFire.Tests 5 | { 6 | public abstract class BaseTest 7 | { 8 | // ReSharper disable once InconsistentNaming 9 | protected readonly AutoMocker _autoMoqer; 10 | 11 | // ReSharper disable once InconsistentNaming 12 | protected readonly Random _random; 13 | 14 | protected BaseTest() 15 | { 16 | _autoMoqer = new AutoMocker(); 17 | _random = new Random(); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/Messages/Inbound/GroupRemove.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using PFire.Core.Session; 3 | 4 | namespace PFire.Core.Protocol.Messages.Inbound 5 | { 6 | internal class GroupRemove : XFireMessage 7 | { 8 | public GroupRemove() : base(XFireMessageType.GroupRemove) { } 9 | 10 | [XMessageField(0x19)] 11 | public int GroupId { get; set; } 12 | 13 | public override async Task Process(IXFireClient context) 14 | { 15 | await context.Server.Database.RemoveGroup(context.User.Id, GroupId); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/Messages/Outbound/GroupsFriends.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace PFire.Core.Protocol.Messages.Outbound 4 | { 5 | internal sealed class GroupsFriends : XFireMessage 6 | { 7 | public GroupsFriends() : base(XFireMessageType.GroupsFriends) 8 | { 9 | UserIds = new List(); 10 | GroupIds = new List(); 11 | } 12 | 13 | [XMessageField(0x01)] 14 | public List UserIds { get; } 15 | 16 | [XMessageField(0x19)] 17 | public List GroupIds { get; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/Messages/Outbound/LoginChallenge.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using PFire.Core.Session; 3 | 4 | namespace PFire.Core.Protocol.Messages.Outbound 5 | { 6 | internal sealed class LoginChallenge : XFireMessage 7 | { 8 | public LoginChallenge() : base(XFireMessageType.LoginChallenge) {} 9 | 10 | [XMessageField("salt")] 11 | public string Salt { get; set; } 12 | 13 | public override Task Process(IXFireClient context) 14 | { 15 | Salt = context.Salt; 16 | 17 | return Task.CompletedTask; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/Messages/Bidirectional/ChatTypingNotification.cs: -------------------------------------------------------------------------------- 1 | namespace PFire.Core.Protocol.Messages.Bidirectional 2 | { 3 | // the typing notification is a sub message from the chat message and 4 | // not a separate message in of itself 5 | 6 | internal sealed class ChatTypingNotification : XFireMessage 7 | { 8 | public ChatTypingNotification() : base(XFireMessageType.ServerChatMessage) {} 9 | 10 | [XMessageField("imindex")] 11 | public int OrderIndex { get; set; } 12 | 13 | [XMessageField("typing")] 14 | public int Typing { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/dotnet-core.yml: -------------------------------------------------------------------------------- 1 | name: .NET Core 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Setup .NET Core 17 | uses: actions/setup-dotnet@v3 18 | with: 19 | dotnet-version: 9.0.x 20 | - name: Install dependencies 21 | run: dotnet restore 22 | - name: Build 23 | run: dotnet build --configuration Release --no-restore 24 | - name: Test 25 | run: dotnet test --no-restore --verbosity normal 26 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/XFireAttributes/NullAttribute.cs: -------------------------------------------------------------------------------- 1 | // unset 2 | 3 | using System; 4 | using System.IO; 5 | 6 | namespace PFire.Core.Protocol.XFireAttributes 7 | { 8 | public class NullAttribute : XFireAttribute 9 | { 10 | public override byte AttributeTypeId => 0x00; 11 | 12 | public override Type AttributeType => typeof(string); 13 | public override dynamic ReadValue(BinaryReader reader) 14 | { 15 | return null; 16 | } 17 | 18 | public override void WriteValue(BinaryWriter writer, dynamic data) 19 | { 20 | // Do nothing 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/XFireAttributes/Int32Attribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace PFire.Core.Protocol.XFireAttributes 5 | { 6 | public class Int32Attribute : XFireAttribute 7 | { 8 | public override byte AttributeTypeId => 0x02; 9 | 10 | public override Type AttributeType => typeof(int); 11 | 12 | public override dynamic ReadValue(BinaryReader reader) 13 | { 14 | return reader.ReadInt32(); 15 | } 16 | 17 | public override void WriteValue(BinaryWriter writer, dynamic data) 18 | { 19 | writer.Write((int)data); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/XFireAttributes/Int8Attribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace PFire.Core.Protocol.XFireAttributes 5 | { 6 | public class Int8Attribute : XFireAttribute 7 | { 8 | public override Type AttributeType => typeof(byte); 9 | 10 | public override byte AttributeTypeId => 0x08; 11 | 12 | public override dynamic ReadValue(BinaryReader reader) 13 | { 14 | return reader.ReadByte(); 15 | } 16 | 17 | public override void WriteValue(BinaryWriter writer, dynamic data) 18 | { 19 | writer.Write((byte)data); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/Messages/Inbound/GameServerFetchAll.cs: -------------------------------------------------------------------------------- 1 | using PFire.Core.Protocol.Messages.Outbound; 2 | using PFire.Core.Session; 3 | using System.Threading.Tasks; 4 | 5 | namespace PFire.Core.Protocol.Messages.Inbound 6 | { 7 | internal sealed class GameServerFetchAll : XFireMessage 8 | { 9 | public GameServerFetchAll() : base(XFireMessageType.GameServerFetchAll) { } 10 | 11 | [XMessageField(0x21)] 12 | public int GameId { get; set; } 13 | 14 | public override async Task Process(IXFireClient context) 15 | { 16 | await context.SendAndProcessMessage(new GameServerSendAll(GameId)); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/Messages/Inbound/GroupRename.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using PFire.Core.Session; 3 | 4 | namespace PFire.Core.Protocol.Messages.Inbound 5 | { 6 | internal class GroupRename : XFireMessage 7 | { 8 | public GroupRename() : base(XFireMessageType.GroupRename) { } 9 | 10 | [XMessageField(0x19)] 11 | public int GroupId { get; set; } 12 | 13 | [XMessageField(0x1A)] 14 | public string Name { get; set; } 15 | 16 | public override async Task Process(IXFireClient context) 17 | { 18 | await context.Server.Database.RenameGroup(context.User.Id, GroupId, Name); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/XFireAttributes/SessionIdAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace PFire.Core.Protocol.XFireAttributes 5 | { 6 | public class SessionIdAttribute : XFireAttribute 7 | { 8 | public override byte AttributeTypeId => 0x03; 9 | 10 | public override Type AttributeType => typeof(Guid); 11 | 12 | public override dynamic ReadValue(BinaryReader reader) 13 | { 14 | return new Guid(reader.ReadBytes(16)); 15 | } 16 | 17 | public override void WriteValue(BinaryWriter writer, dynamic data) 18 | { 19 | writer.Write(((Guid)data).ToByteArray()); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/PFire.Core/ITcpServer.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using PFire.Core.Protocol.Messages; 3 | using PFire.Core.Session; 4 | 5 | namespace PFire.Core 6 | { 7 | internal interface ITcpServer 8 | { 9 | delegate Task OnConnectionHandler(IXFireClient sessionContext); 10 | 11 | delegate Task OnDisconnectionHandler(IXFireClient sessionContext); 12 | 13 | delegate Task OnReceiveHandler(IXFireClient sessionContext, IMessage message); 14 | 15 | event OnReceiveHandler OnReceive; 16 | event OnConnectionHandler OnConnection; 17 | event OnDisconnectionHandler OnDisconnection; 18 | Task Listen(); 19 | void Shutdown(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/XFireAttributes/DidAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace PFire.Core.Protocol.XFireAttributes 5 | { 6 | // TODO: Should have its own type and not byte[] 7 | public class DidAttribute : XFireAttribute 8 | { 9 | public override byte AttributeTypeId => 0x06; 10 | 11 | public override Type AttributeType => typeof(byte[]); 12 | 13 | public override dynamic ReadValue(BinaryReader reader) 14 | { 15 | return reader.ReadBytes(21); 16 | } 17 | 18 | public override void WriteValue(BinaryWriter writer, dynamic data) 19 | { 20 | writer.Write((byte[])data); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/Messages/Inbound/GameServerFetchFriendsFavorites.cs: -------------------------------------------------------------------------------- 1 | using PFire.Core.Protocol.Messages.Outbound; 2 | using PFire.Core.Session; 3 | using System.Threading.Tasks; 4 | 5 | namespace PFire.Core.Protocol.Messages.Inbound 6 | { 7 | internal sealed class GameServerFetchFriendsFavorites : XFireMessage 8 | { 9 | public GameServerFetchFriendsFavorites() : base(XFireMessageType.GameServerFetchFriendsFavorites) { } 10 | 11 | [XMessageField("gameid")] 12 | public int GameId { get; set; } 13 | 14 | public override async Task Process(IXFireClient context) 15 | { 16 | await context.SendAndProcessMessage(new GameServerSendFriendsFavorites(GameId)); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/Messages/Inbound/KeepAlive.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using PFire.Core.Protocol.Messages.Outbound; 4 | using PFire.Core.Session; 5 | 6 | namespace PFire.Core.Protocol.Messages.Inbound 7 | { 8 | internal sealed class KeepAlive : XFireMessage 9 | { 10 | public KeepAlive() : base(XFireMessageType.KeepAlive) { } 11 | 12 | [XMessageField("value")] 13 | public int? Value { get; set; } 14 | 15 | [XMessageField("stats")] 16 | public List Stats { get; set; } 17 | 18 | public async override Task Process(IXFireClient client) 19 | { 20 | await client.SendAndProcessMessage(new ServerPong()); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/XMessageField.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | 4 | namespace PFire.Core.Protocol 5 | { 6 | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Method | AttributeTargets.Property, AllowMultiple = true)] 7 | internal sealed class XMessageField : Attribute 8 | { 9 | public string Name { get; } 10 | public byte[] NameAsBytes => Encoding.UTF8.GetBytes(Name); 11 | public bool NonTextualName { get; } 12 | 13 | public XMessageField(string name) 14 | { 15 | Name = name; 16 | } 17 | 18 | public XMessageField(params byte[] name) 19 | : this(Encoding.UTF8.GetString(name)) 20 | { 21 | NonTextualName = true; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/PFire.Console/Services/PFireServerService.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using Microsoft.Extensions.Hosting; 4 | using PFire.Core; 5 | 6 | namespace PFire.Console.Services 7 | { 8 | internal class PFireServerService : IHostedService 9 | { 10 | private readonly IPFireServer _pfServer; 11 | 12 | public PFireServerService(IPFireServer pFireServer) 13 | { 14 | _pfServer = pFireServer; 15 | } 16 | 17 | public Task StartAsync(CancellationToken cancellationToken) 18 | { 19 | return _pfServer.Start(); 20 | } 21 | 22 | public Task StopAsync(CancellationToken cancellationToken) 23 | { 24 | return _pfServer.Stop(); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/Messages/XFireMessage.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.Extensions.Logging; 3 | using PFire.Core.Session; 4 | 5 | namespace PFire.Core.Protocol.Messages 6 | { 7 | internal abstract class XFireMessage : IMessage 8 | { 9 | protected XFireMessage(XFireMessageType typeId) 10 | { 11 | MessageTypeId = typeId; 12 | } 13 | 14 | public XFireMessageType MessageTypeId { get; } 15 | 16 | public virtual Task Process(IXFireClient client) 17 | { 18 | // base implementation is to do nothing 19 | client.Logger.LogWarning($" *** Unimplemented processing for message type {MessageTypeId}"); 20 | 21 | return Task.CompletedTask; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/PFire.Infrastructure/PFire.Infrastructure.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net10.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | all 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/PFire.Console/Extensions/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using PFire.Common.Extensions; 4 | using PFire.Console.Services; 5 | using PFire.Core.Extensions; 6 | using PFire.Infrastructure.Extensions; 7 | 8 | namespace PFire.Console.Extensions 9 | { 10 | internal static class ServiceCollectionExtensions 11 | { 12 | public static IServiceCollection RegisterAll(this IServiceCollection services, IConfiguration configuration) 13 | { 14 | return services 15 | .AddHostedService() 16 | .RegisterCore() 17 | .RegisterInfrastructure(configuration) 18 | .RegisterCommon(); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/Messages/Inbound/GroupCreate.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using PFire.Core.Protocol.Messages.Outbound; 3 | using PFire.Core.Session; 4 | 5 | namespace PFire.Core.Protocol.Messages.Inbound 6 | { 7 | internal class GroupCreate : XFireMessage 8 | { 9 | public GroupCreate() : base(XFireMessageType.GroupCreate) { } 10 | 11 | [XMessageField(0x1A)] 12 | public string Name { get; set; } 13 | 14 | public override async Task Process(IXFireClient context) 15 | { 16 | var group = await context.Server.Database.CreateGroup(context.User, Name); 17 | if(group is not null) 18 | { 19 | await context.SendAndProcessMessage(new GroupCreateConfirmation(group)); 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/Messages/Outbound/FriendStatusChange.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace PFire.Core.Protocol.Messages.Outbound 5 | { 6 | internal sealed class FriendStatusChange : XFireMessage 7 | { 8 | public FriendStatusChange(Guid sessionId, string message) : base(XFireMessageType.FriendStatusChange) 9 | { 10 | SessionIds = new List 11 | { 12 | sessionId 13 | }; 14 | 15 | Messages = new List 16 | { 17 | message 18 | }; 19 | } 20 | 21 | [XMessageField("sid")] 22 | public List SessionIds { get; } 23 | 24 | [XMessageField("msg")] 25 | public List Messages { get; } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/Messages/Outbound/ServerList.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace PFire.Core.Protocol.Messages.Outbound 4 | { 5 | internal sealed class ServerList : XFireMessage 6 | { 7 | public ServerList() : base(XFireMessageType.ServerList) 8 | { 9 | GameIds = new List(); 10 | GameIPs = new List(); 11 | GamePorts = new List(); 12 | } 13 | 14 | [XMessageField("max")] 15 | public int MaximumFavorites { get; set; } 16 | 17 | [XMessageField("gameid")] 18 | public List GameIds { get; } 19 | 20 | [XMessageField("gip")] 21 | public List GameIPs { get; } 22 | 23 | [XMessageField("gport")] 24 | public List GamePorts { get; } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/PFire.Infrastructure/Services/DatabaseContextFactory.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Design; 3 | 4 | namespace PFire.Infrastructure.Services 5 | { 6 | /// 7 | /// Used to generate a for creating migrations. 8 | /// 9 | // ReSharper disable once UnusedType.Global 10 | internal class DatabaseContextFactory : IDesignTimeDbContextFactory 11 | { 12 | public DatabaseContext CreateDbContext(string[] args) 13 | { 14 | var optionsBuilder = new DbContextOptionsBuilder(); 15 | optionsBuilder.UseSqlite("Data Source=pfiredb.sqlite"); 16 | 17 | return new DatabaseContext(optionsBuilder.Options); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/Messages/Outbound/SystemBroadcastMessage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace PFire.Core.Protocol.Messages.Outbound 5 | { 6 | internal sealed class SystemBroadcastMessage : XFireMessage 7 | { 8 | public SystemBroadcastMessage(string message) : base(XFireMessageType.SystemBroadcast) 9 | { 10 | Message = message; 11 | } 12 | 13 | // Not entirely sure what this does. I looked at decompiled code and it seems to make it 14 | // skip over reading a string if set to zero or something. However even when set to zero 15 | // it still displays. 16 | [XMessageField(0x34)] 17 | public int Unk { get; set; } 18 | 19 | [XMessageField(0x2E)] 20 | public string Message { get; set; } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/Messages/Outbound/ServerPong.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using PFire.Core.Session; 7 | 8 | /// 9 | /// Packet 144 - Server to Client PONG response 10 | /// Attributes: 11 | /// value - Always zero, I suspect that they just need one attribute. 12 | /// 13 | 14 | namespace PFire.Core.Protocol.Messages.Outbound 15 | { 16 | internal sealed class ServerPong : XFireMessage 17 | { 18 | public ServerPong() : base(XFireMessageType.ServerPong) { } 19 | 20 | [XMessageField("value")] 21 | public int Value { get; set; } = 0; 22 | 23 | public override Task Process(IXFireClient context) 24 | { 25 | return Task.CompletedTask; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/Messages/Outbound/GroupCreateConfirmation.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using PFire.Core.Models; 3 | using PFire.Core.Session; 4 | 5 | namespace PFire.Core.Protocol.Messages.Outbound 6 | { 7 | internal class GroupCreateConfirmation : XFireMessage 8 | { 9 | public GroupCreateConfirmation(GroupModel group) : base(XFireMessageType.GroupCreateConfirmation) 10 | { 11 | Id = group.Id; 12 | Name = group.Name; 13 | } 14 | 15 | [XMessageField(0x19)] 16 | public int Id { get; set; } 17 | 18 | [XMessageField(0x1A)] 19 | public string Name { get; set; } 20 | 21 | public override Task Process(IXFireClient context) 22 | { 23 | //nothing to see here bud 24 | return Task.CompletedTask; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/PFire.Core/Util/LoggerExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Microsoft.Extensions.Logging; 3 | using PFire.Core.Models; 4 | using PFire.Core.Protocol.Messages; 5 | 6 | namespace PFire.Core.Util 7 | { 8 | internal static class LoggerExtensions 9 | { 10 | private static readonly IList IgnoreMessageIds = new[] {XFireMessageType.KeepAlive}; 11 | 12 | public static void LogXFireMessage(this ILogger logger, IMessage message, UserModel user) 13 | { 14 | if (IgnoreMessageIds.Contains(message.MessageTypeId)) 15 | { 16 | return; 17 | } 18 | 19 | var username = user?.Username ?? "unknown"; 20 | var userId = user?.Id ?? -1; 21 | 22 | logger.LogDebug($"Sent message[{username},{userId}]: {message}"); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/PFire.Infrastructure/Entities/Group.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 2 | 3 | namespace PFire.Infrastructure.Entities 4 | { 5 | public class Group : Entity 6 | { 7 | public int Id { get; set; } 8 | public string Name { get; set; } 9 | 10 | public int OwnerId { get; set; } 11 | public User Owner { get; set; } 12 | } 13 | 14 | internal class GroupConfiguration : EntityConfiguration 15 | { 16 | protected override void ConfigureEntity(EntityTypeBuilder builder) 17 | { 18 | //builder.ToTable(nameof(Group)); 19 | 20 | //builder.HasKey(x => x.Id); 21 | //builder.Property(x => x.Id).ValueGeneratedOnAdd(); 22 | //builder.HasIndex(x => x.Id); 23 | 24 | builder.Property(x => x.Name).IsRequired().HasMaxLength(1000); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/PFire.Core/Extensions/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Net.Sockets; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using PFire.Core.Services; 5 | using PFire.Core.Session; 6 | 7 | namespace PFire.Core.Extensions 8 | { 9 | public static class ServiceCollectionExtensions 10 | { 11 | public static IServiceCollection RegisterCore(this IServiceCollection serviceCollection) 12 | { 13 | return serviceCollection.AddSingleton() 14 | .AddSingleton() 15 | .AddSingleton() 16 | .AddSingleton() 17 | .AddSingleton(x => new TcpListener(new IPEndPoint(IPAddress.Any, 25999))); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/PFire.Infrastructure/Extensions/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.Extensions.Configuration; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using PFire.Infrastructure.Services; 5 | 6 | namespace PFire.Infrastructure.Extensions 7 | { 8 | public static class ServiceCollectionExtensions 9 | { 10 | public static IServiceCollection RegisterInfrastructure(this IServiceCollection serviceCollection, IConfiguration configuration) 11 | { 12 | var connectionString = configuration.GetConnectionString("PFire"); 13 | 14 | return serviceCollection.AddScoped() 15 | .AddScoped() 16 | .AddDbContext(options => options.UseSqlite(connectionString)); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/PFire.Core/Enums/ClientPreferences.cs: -------------------------------------------------------------------------------- 1 | namespace PFire.Core.Enums 2 | { 3 | internal enum ClientPreferences 4 | { 5 | GameStatusShowMyFriends = 1, 6 | GameStatusShowMyGameServer = 2, 7 | GameStatusShowMyProfile = 3, 8 | PlaySoundSendOrReceiveMessage = 4, 9 | PlaySoundReceiveMessageWhileGaming = 5, 10 | ChatShowTimestamps = 6, 11 | PlaySoundFriendLogsOnOff = 7, 12 | GameStatusShowFriendOfFriends = 8, 13 | ShowOfflineFriends = 9, 14 | ShowNicknames = 10, 15 | ShowVoiceChatServerToFriends = 11, 16 | ShowWhenTyping = 12, 17 | NotificationFriendLogsOnOff = 16, 18 | NotificationDownloadStartsFinishes = 17, 19 | PlaySoundSomeoneJoinsLeaveChatroom = 18, 20 | PlaySoundSendReceiveVoiceChatRequest = 19, 21 | PlaySoundScreenshotWhileGaming = 20, 22 | NotificationConnectionStateChanges = 21 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/Messages/Inbound/ClientConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using PFire.Core.Protocol.Messages.Outbound; 3 | using PFire.Core.Session; 4 | 5 | namespace PFire.Core.Protocol.Messages.Inbound 6 | { 7 | internal sealed class ClientConfiguration : XFireMessage 8 | { 9 | public ClientConfiguration() : base(XFireMessageType.ClientConfiguration) {} 10 | 11 | [XMessageField("lang")] 12 | public string Language { get; set; } 13 | 14 | [XMessageField("skin")] 15 | public string Skin { get; set; } 16 | 17 | [XMessageField("theme")] 18 | public string Theme { get; set; } 19 | 20 | [XMessageField("partner")] 21 | public string Partner { get; set; } 22 | 23 | public override async Task Process(IXFireClient context) 24 | { 25 | await context.SendAndProcessMessage(new Did()); 26 | 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/XFireAttributes/StringAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text; 4 | 5 | namespace PFire.Core.Protocol.XFireAttributes 6 | { 7 | public class StringAttribute : XFireAttribute 8 | { 9 | public override byte AttributeTypeId => 0x01; 10 | 11 | public override Type AttributeType => typeof(string); 12 | 13 | public override dynamic ReadValue(BinaryReader reader) 14 | { 15 | var valueLength = reader.ReadInt16(); 16 | var bytes = reader.ReadBytes(valueLength); 17 | return Encoding.UTF8.GetString(bytes); 18 | } 19 | 20 | public override void WriteValue(BinaryWriter writer, dynamic data) 21 | { 22 | var value = (string)data ?? string.Empty; 23 | 24 | writer.Write((short)value.Length); 25 | writer.Write(Encoding.UTF8.GetBytes(value)); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/PFire.Tests/PFire.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net10.0 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | all 16 | runtime; build; native; contentfiles; analyzers 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/Messages/Inbound/UserLookup.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using PFire.Core.Protocol.Messages.Outbound; 3 | using PFire.Core.Session; 4 | 5 | namespace PFire.Core.Protocol.Messages.Inbound 6 | { 7 | internal sealed class UserLookup : XFireMessage 8 | { 9 | public UserLookup() : base(XFireMessageType.UserLookup) {} 10 | 11 | [XMessageField("name")] 12 | public string Username { get; set; } 13 | 14 | [XMessageField("fname")] 15 | public string FirstName { get; set; } 16 | 17 | [XMessageField("lname")] 18 | public string LastName { get; set; } 19 | 20 | [XMessageField("email")] 21 | public string Email { get; set; } 22 | 23 | public override async Task Process(IXFireClient context) 24 | { 25 | var result = new UserLookupResult(Username); 26 | await context.SendAndProcessMessage(result); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/Messages/Inbound/ClientVersion.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using PFire.Core.Protocol.Messages.Outbound; 3 | using PFire.Core.Session; 4 | 5 | namespace PFire.Core.Protocol.Messages.Inbound 6 | { 7 | internal sealed class ClientVersion : XFireMessage 8 | { 9 | public ClientVersion() : base(XFireMessageType.ClientVersion) {} 10 | 11 | [XMessageField("version")] 12 | public int Version { get; set; } 13 | 14 | [XMessageField("major_version")] 15 | public int MajorVersion { get; set; } 16 | 17 | public override async Task Process(IXFireClient context) 18 | { 19 | var loginChallenge = new LoginChallenge(); 20 | await loginChallenge.Process(context); 21 | await context.SendMessage(loginChallenge); 22 | } 23 | 24 | public override string ToString() 25 | { 26 | return $"[ClientVersion] v: {Version} mv: {MajorVersion}"; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/Messages/Inbound/FriendRequestDecline.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading.Tasks; 3 | using PFire.Core.Session; 4 | 5 | namespace PFire.Core.Protocol.Messages.Inbound 6 | { 7 | internal sealed class FriendRequestDecline : XFireMessage 8 | { 9 | public FriendRequestDecline() : base(XFireMessageType.FriendRequestDecline) {} 10 | 11 | [XMessageField("name")] 12 | public string RequesterUsername { get; set; } 13 | 14 | public override async Task Process(IXFireClient context) 15 | { 16 | var requesterUser = await context.Server.Database.QueryUser(RequesterUsername); 17 | var pendingRequests = await context.Server.Database.QueryPendingFriendRequestsSelf(requesterUser); 18 | 19 | var requestsIds = pendingRequests.Where(a => a.Id == requesterUser.Id).ToArray(); 20 | 21 | await context.Server.Database.DeletePendingFriendRequest(context.User, requestsIds); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/Messages/Outbound/FriendInvite.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace PFire.Core.Protocol.Messages.Outbound 4 | { 5 | internal sealed class FriendInvite : XFireMessage 6 | { 7 | public FriendInvite(string username, string nickname, string message) : base(XFireMessageType.FriendInvite) 8 | { 9 | Usernames = new List 10 | { 11 | username 12 | }; 13 | 14 | Nicknames = new List 15 | { 16 | nickname 17 | }; 18 | 19 | Messages = new List 20 | { 21 | message 22 | }; 23 | } 24 | 25 | [XMessageField("name")] 26 | public List Usernames { get; } 27 | 28 | [XMessageField("nick")] 29 | public List Nicknames { get; } 30 | 31 | [XMessageField("msg")] 32 | public List Messages { get; } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/PFire.Tests/PFire.Core/Protocol/MessageSerializerTests.cs: -------------------------------------------------------------------------------- 1 | using PFire.Core.Protocol; 2 | using PFire.Core.Protocol.Messages; 3 | using PFire.Core.Protocol.Messages.Inbound; 4 | using Xunit; 5 | 6 | namespace PFire.Tests.PFire.Core.Protocol 7 | { 8 | public class MessageSerializerTests 9 | { 10 | [Theory] 11 | [InlineData(new byte[] { 0x0D, 0x00, 0x02, 0x05, 0x76, 0x61, 0x6C, 0x75, 0x65, 0x02, 0x00, 0x00, 0x00, 0x00, 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x04, 0x02, 0x00, 0x00 })] 12 | public void Deserialize_KeepAlive_ReturnsMessage(byte[] bytes) 13 | { 14 | var message = MessageSerializer.Deserialize(bytes); 15 | 16 | Assert.True(message.MessageTypeId == XFireMessageType.KeepAlive); 17 | Assert.True(typeof(KeepAlive) == message.GetType()); 18 | 19 | var keepAlive = message as KeepAlive; 20 | Assert.True(keepAlive.Value == 0); 21 | Assert.True(keepAlive.Stats.Count == 0); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/Messages/Outbound/Groups.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using PFire.Core.Session; 4 | 5 | namespace PFire.Core.Protocol.Messages.Outbound 6 | { 7 | internal sealed class Groups : XFireMessage 8 | { 9 | public Groups() : base(XFireMessageType.Groups) {} 10 | 11 | [XMessageField(0x19)] 12 | public List GroupIds { get; set; } 13 | 14 | [XMessageField(0x1a)] 15 | public List GroupNames { get; set; } 16 | 17 | public override async Task Process(IXFireClient context) 18 | { 19 | GroupIds = new List(); 20 | GroupNames = new List(); 21 | 22 | var groups = await context.Server.Database.GetGroupsByOwner(context.User.Id); 23 | 24 | foreach (var group in groups) 25 | { 26 | GroupIds.Add(group.Id); 27 | GroupNames.Add(group.Name); 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/Messages/Outbound/Did.cs: -------------------------------------------------------------------------------- 1 | namespace PFire.Core.Protocol.Messages.Outbound 2 | { 3 | internal sealed class Did : XFireMessage 4 | { 5 | public Did() : base(XFireMessageType.Did) 6 | { 7 | // this is supposed to be read from the server 8 | Unknown = new byte[] 9 | { 10 | 0xd1, 11 | 0xc2, 12 | 0x95, 13 | 0x33, 14 | 0x84, 15 | 0xc4, 16 | 0xcc, 17 | 0xb2, 18 | 0x31, 19 | 0x50, 20 | 0x1f, 21 | 0x0e, 22 | 0x43, 23 | 0xc5, 24 | 0x89, 25 | 0x30, 26 | 0xb2, 27 | 0xa7, 28 | 0x0e, 29 | 0x4a, 30 | 0xb6 31 | }; 32 | } 33 | 34 | [XMessageField("did")] 35 | public byte[] Unknown { get; } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/PFire.Core/Util/Disposable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PFire.Core.Util 4 | { 5 | public abstract class Disposable : IDisposable 6 | { 7 | protected bool Disposed; 8 | protected bool IsDisposing; 9 | protected virtual void Dispose(bool disposing) 10 | { 11 | if(Disposed) 12 | { 13 | return; 14 | } 15 | 16 | if (disposing) 17 | { 18 | try 19 | { 20 | IsDisposing = true; 21 | DisposeManagedResources(); 22 | } 23 | finally 24 | { 25 | IsDisposing = false; 26 | } 27 | } 28 | 29 | Disposed = true; 30 | } 31 | 32 | public void Dispose() 33 | { 34 | Dispose(true); 35 | GC.SuppressFinalize(this); 36 | } 37 | 38 | protected virtual void DisposeManagedResources() { } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/PFire.Infrastructure/Entities/Entity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 4 | 5 | namespace PFire.Infrastructure.Entities 6 | { 7 | public abstract class Entity 8 | { 9 | public byte[] Version { get; set; } 10 | public DateTime DateCreated { get; set; } 11 | public DateTime? DateModified { get; set; } 12 | } 13 | 14 | internal abstract class EntityConfiguration : IEntityTypeConfiguration where T : Entity 15 | { 16 | public virtual void Configure(EntityTypeBuilder builder) 17 | { 18 | builder.Property(x => x.Version).IsConcurrencyToken().ValueGeneratedOnAddOrUpdate(); 19 | builder.Property(x => x.DateCreated).IsRequired(); 20 | builder.Property(x => x.DateModified).IsRequired(false); 21 | 22 | ConfigureEntity(builder); 23 | } 24 | 25 | protected abstract void ConfigureEntity(EntityTypeBuilder entityTypeBuilder); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/Messages/Inbound/FriendRemoval.cs: -------------------------------------------------------------------------------- 1 | using PFire.Core.Protocol.Messages.Outbound; 2 | using PFire.Core.Session; 3 | using System.Threading.Tasks; 4 | 5 | namespace PFire.Core.Protocol.Messages.Inbound 6 | { 7 | internal sealed class FriendRemoval : XFireMessage 8 | { 9 | public FriendRemoval() : base(XFireMessageType.FriendRemoval) { } 10 | 11 | [XMessageField("userid")] 12 | public int UserId { get; set; } 13 | 14 | public override async Task Process(IXFireClient context) 15 | { 16 | var friend = context.Server.Database.QueryUser(UserId); 17 | await context.Server.Database.RemoveFriend(context.User, friend.Result); 18 | await context.SendAndProcessMessage(new FriendRemoved(UserId)); 19 | 20 | var friendSession = context.Server.GetSession(friend.Result); 21 | if (friendSession != null) 22 | { 23 | await friendSession.SendAndProcessMessage(new FriendRemoved(context.User.Id)); 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/Messages/Inbound/StatusChange.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using PFire.Core.Protocol.Messages.Outbound; 3 | using PFire.Core.Session; 4 | 5 | namespace PFire.Core.Protocol.Messages.Inbound 6 | { 7 | internal sealed class StatusChange : XFireMessage 8 | { 9 | public StatusChange() : base(XFireMessageType.StatusChange) {} 10 | 11 | [XMessageField(0x2e)] 12 | public string Message { get; set; } 13 | 14 | public override async Task Process(IXFireClient context) 15 | { 16 | var statusChange = new FriendStatusChange(context.SessionId, Message); 17 | var friends = await context.Server.Database.QueryFriends(context.User); 18 | foreach (var friend in friends) 19 | { 20 | var friendSession = context.Server.GetSession(friend); 21 | if (friendSession != null) 22 | { 23 | await friendSession.SendAndProcessMessage(statusChange); 24 | } 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Darcy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/Messages/Inbound/FriendRequest.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using PFire.Core.Protocol.Messages.Outbound; 3 | using PFire.Core.Session; 4 | 5 | namespace PFire.Core.Protocol.Messages.Inbound 6 | { 7 | internal sealed class FriendRequest : XFireMessage 8 | { 9 | public FriendRequest() : base(XFireMessageType.FriendRequest) {} 10 | 11 | [XMessageField("name")] 12 | public string Username { get; set; } 13 | 14 | [XMessageField("msg")] 15 | public string Message { get; set; } 16 | 17 | public override async Task Process(IXFireClient context) 18 | { 19 | var recipient = await context.Server.Database.QueryUser(Username); 20 | var invite = new FriendInvite(context.User.Username, context.User.Nickname, Message); 21 | await invite.Process(context); 22 | 23 | await context.Server.Database.InsertFriendRequest(context.User, recipient, Message); 24 | 25 | var recipientSession = context.Server.GetSession(recipient); 26 | if (recipientSession != null) 27 | { 28 | await recipientSession.SendMessage(invite); 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/PFire.Core/Models/ClientPreferencesModel.cs: -------------------------------------------------------------------------------- 1 | namespace PFire.Core.Models 2 | { 3 | public class ClientPreferencesModel 4 | { 5 | public bool GameStatusShowMyFriends { get; set; } 6 | public bool GameStatusShowMyGameServer { get; set; } 7 | public bool GameStatusShowMyProfile { get; set; } 8 | public bool ChatShowTimestamps { get; set; } 9 | public bool ShowVoiceChatServerToFriends { get; set; } 10 | public bool ShowWhenTyping { get; set; } 11 | public bool GameStatusShowFriendOfFriends { get; set; } 12 | public bool PlaySoundSendOrReceiveMessage { get; set; } 13 | public bool PlaySoundReceiveMessageWhileGaming { get; set; } 14 | public bool PlaySoundFriendLogsOnOff { get; set; } 15 | public bool ShowOfflineFriends { get; set; } 16 | public bool ShowNicknames { get; set; } 17 | public bool NotificationFriendLogsOnOff { get; set; } 18 | public bool NotificationDownloadStartsFinishes { get; set; } 19 | public bool PlaySoundSomeoneJoinsLeaveChatroom { get; set; } 20 | public bool PlaySoundSendReceiveVoiceChatRequest { get; set; } 21 | public bool PlaySoundScreenshotWhileGaming { get; set; } 22 | public bool NotificationConnectionStateChanges { get; set; } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/Messages/Outbound/FriendsList.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using PFire.Core.Models; 4 | using PFire.Core.Session; 5 | 6 | namespace PFire.Core.Protocol.Messages.Outbound 7 | { 8 | internal sealed class FriendsList : XFireMessage 9 | { 10 | private readonly UserModel _ownerUser; 11 | 12 | public FriendsList(UserModel owner) : base(XFireMessageType.FriendsList) 13 | { 14 | _ownerUser = owner; 15 | 16 | UserIds = new List(); 17 | Usernames = new List(); 18 | Nicks = new List(); 19 | } 20 | 21 | [XMessageField("userid")] 22 | public List UserIds { get; } 23 | 24 | [XMessageField("friends")] 25 | public List Usernames { get; } 26 | 27 | [XMessageField("nick")] 28 | public List Nicks { get; } 29 | 30 | public override async Task Process(IXFireClient context) 31 | { 32 | var friends = await context.Server.Database.QueryFriends(_ownerUser); 33 | friends.ForEach(f => 34 | { 35 | UserIds.Add(f.Id); 36 | Usernames.Add(f.Username); 37 | Nicks.Add(f.Nickname); 38 | }); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/Messages/Outbound/GameServerSendAll.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Net; 6 | using System.Threading.Tasks; 7 | using PFire.Core.Session; 8 | 9 | namespace PFire.Core.Protocol.Messages.Outbound 10 | { 11 | internal sealed class GameServerSendAll : XFireMessage 12 | { 13 | public GameServerSendAll(int gameId) : base(XFireMessageType.GameServerSendAll) 14 | { 15 | GameId = gameId; 16 | GameIps = new List(); 17 | GamePorts = new List(); 18 | } 19 | 20 | [XMessageField(0x21)] 21 | public int GameId { get; set; } 22 | 23 | [XMessageField(0x22)] 24 | public List GameIps { get; set; } 25 | 26 | [XMessageField(0x23)] 27 | public List GamePorts { get; set; } 28 | 29 | public override Task Process(IXFireClient context) 30 | { 31 | //TODO: Have a Database of IPs and Ports that is fetched by gameid 32 | // Send back the GameId sent 33 | // Iterate that into Ips and Ports (unsigned ints on both) 34 | // If no hits, send with empty Lists regardless, because the client expects a response. 35 | 36 | return Task.CompletedTask; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/XFireAttributes/XFireAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text; 4 | 5 | namespace PFire.Core.Protocol.XFireAttributes 6 | { 7 | public abstract class XFireAttribute 8 | { 9 | public abstract Type AttributeType { get; } 10 | public abstract byte AttributeTypeId { get; } 11 | public abstract dynamic ReadValue(BinaryReader reader); 12 | public abstract void WriteValue(BinaryWriter writer, dynamic data); 13 | 14 | public void WriteType(BinaryWriter writer) 15 | { 16 | writer.Write(AttributeTypeId); 17 | } 18 | 19 | public void WriteName(BinaryWriter writer, string name) 20 | { 21 | if(name == null) 22 | { 23 | return; 24 | } 25 | 26 | writer.Write((byte)name.Length); 27 | WriteNameWithoutLengthPrefix(writer, Encoding.UTF8.GetBytes(name)); 28 | } 29 | 30 | public void WriteNameWithoutLengthPrefix(BinaryWriter writer, byte[] name) 31 | { 32 | writer.Write(name); 33 | } 34 | 35 | public void WriteAll(BinaryWriter writer, string name, dynamic data) 36 | { 37 | WriteName(writer, name); 38 | WriteType(writer); 39 | WriteValue(writer, data); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/Messages/Inbound/NicknameChange.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using PFire.Core.Protocol.Messages.Outbound; 3 | using PFire.Core.Session; 4 | 5 | namespace PFire.Core.Protocol.Messages.Inbound 6 | { 7 | internal sealed class NicknameChange : XFireMessage 8 | { 9 | private const int MAX_LENGTH = 35; 10 | 11 | public NicknameChange() : base(XFireMessageType.NicknameChange) {} 12 | 13 | [XMessageField("nick")] 14 | public string Nickname { get; set; } 15 | 16 | public override async Task Process(IXFireClient context) 17 | { 18 | if (Nickname.Length > MAX_LENGTH) 19 | { 20 | Nickname = Nickname.Substring(0, MAX_LENGTH); 21 | } 22 | 23 | await context.Server.Database.UpdateNickname(context.User, Nickname); 24 | 25 | var updatedFriendsList = new FriendsList(context.User); 26 | var queryFriends = await context.Server.Database.QueryFriends(context.User); 27 | foreach (var friend in queryFriends) 28 | { 29 | var friendSession = context.Server.GetSession(friend); 30 | if (friendSession != null) 31 | { 32 | await friendSession.SendAndProcessMessage(updatedFriendsList); 33 | } 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/PFire.Tests/PFire.Console/Services/PFireServerServiceTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Moq; 3 | using PFire.Console.Services; 4 | using PFire.Core; 5 | using Xunit; 6 | 7 | namespace PFire.Tests.PFire.Console.Services 8 | { 9 | public class PFireServerServiceTests : BaseTest 10 | { 11 | [Fact] 12 | public async Task StartAsync_Calls_Start() 13 | { 14 | //arrange 15 | var pFireServerMock = _autoMoqer.GetMock(); 16 | 17 | var service = _autoMoqer.CreateInstance(); 18 | 19 | //act 20 | await service.StartAsync(default); 21 | 22 | //assert 23 | pFireServerMock.Verify(x => x.Start(), Times.Once); 24 | pFireServerMock.Verify(x => x.Stop(), Times.Never); 25 | } 26 | 27 | [Fact] 28 | public async Task StopAsync_Calls_Stop() 29 | { 30 | //arrange 31 | var pFireServerMock = _autoMoqer.GetMock(); 32 | 33 | var service = _autoMoqer.CreateInstance(); 34 | 35 | //act 36 | await service.StopAsync(default); 37 | 38 | //assert 39 | pFireServerMock.Verify(x => x.Start(), Times.Never); 40 | pFireServerMock.Verify(x => x.Stop(), Times.Once); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/PFire.Infrastructure/Entities/User.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 4 | 5 | namespace PFire.Infrastructure.Entities 6 | { 7 | public class User : Entity 8 | { 9 | public int Id { get; set; } 10 | public string Username { get; set; } 11 | public string Password { get; set; } 12 | public string Salt { get; set; } 13 | public string Nickname { get; set; } 14 | public List MyFriends { get; set; } 15 | public List FriendsOf { get; set; } 16 | public ClientPreferences ClientPreferences { get; set; } 17 | } 18 | 19 | internal class UserConfiguration : EntityConfiguration 20 | { 21 | protected override void ConfigureEntity(EntityTypeBuilder builder) 22 | { 23 | builder.ToTable(nameof(User)); 24 | builder.HasKey(x => x.Id); 25 | builder.Property(x => x.Id).ValueGeneratedOnAdd(); 26 | builder.HasIndex(x => x.Id); 27 | builder.Property(x => x.Username).IsRequired().HasMaxLength(1000); 28 | builder.HasIndex(x => x.Username); 29 | builder.Property(x => x.Password).IsRequired(); 30 | builder.Property(x => x.Salt).IsRequired(); 31 | builder.Property(x => x.Nickname).HasMaxLength(1000); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/PFire.Infrastructure/Entities/ClientPreferences.cs: -------------------------------------------------------------------------------- 1 | namespace PFire.Infrastructure.Entities 2 | { 3 | public class ClientPreferences : Entity 4 | { 5 | public int Id { get; set; } 6 | public int UserId { get; set; } 7 | public User User { get; set; } 8 | 9 | public bool GameStatusShowMyFriends { get; set; } 10 | public bool GameStatusShowMyGameServer { get; set; } 11 | public bool GameStatusShowMyProfile { get; set; } 12 | public bool ChatShowTimestamps { get; set; } 13 | public bool ShowVoiceChatServerToFriends { get; set; } 14 | public bool ShowWhenTyping { get; set; } 15 | public bool GameStatusShowFriendOfFriends { get; set; } 16 | public bool PlaySoundSendOrReceiveMessage { get; set; } 17 | public bool PlaySoundReceiveMessageWhileGaming { get; set; } 18 | public bool PlaySoundFriendLogsOnOff { get; set; } 19 | public bool ShowOfflineFriends { get; set; } 20 | public bool ShowNicknames { get; set; } 21 | public bool NotificationFriendLogsOnOff { get; set; } 22 | public bool NotificationDownloadStartsFinishes { get; set; } 23 | public bool PlaySoundSomeoneJoinsLeaveChatroom { get; set; } 24 | public bool PlaySoundSendReceiveVoiceChatRequest { get; set; } 25 | public bool PlaySoundScreenshotWhileGaming { get; set; } 26 | public bool NotificationConnectionStateChanges { get; set; } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/PFire.Console/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.Hosting; 4 | using PFire.Console.Extensions; 5 | using PFire.Infrastructure.Services; 6 | using Serilog; 7 | 8 | namespace PFire.Console 9 | { 10 | public class Program 11 | { 12 | public static async Task Main(string[] args) 13 | { 14 | var host = CreateHostBuilder(args).Build(); 15 | 16 | await MigrateDatabase(host); 17 | 18 | await host.RunAsync(); 19 | } 20 | 21 | private static async Task MigrateDatabase(IHost host) 22 | { 23 | using var scope = host.Services.CreateScope(); 24 | 25 | await scope.ServiceProvider.GetRequiredService().Migrate(); 26 | } 27 | 28 | private static IHostBuilder CreateHostBuilder(string[] args) 29 | { 30 | return Host.CreateDefaultBuilder(args) 31 | .UseSerilog((hostingContext, loggerConfiguration) => 32 | { 33 | loggerConfiguration.ReadFrom.Configuration(hostingContext.Configuration); 34 | }) 35 | .UseWindowsService() 36 | .UseSystemd() 37 | .ConfigureServices((hostBuilderContext, services) => 38 | { 39 | services.RegisterAll(hostBuilderContext.Configuration); 40 | }); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/Messages/Inbound/GameInformation.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using PFire.Core.Session; 3 | using PFire.Core.Protocol.Messages.Outbound; 4 | 5 | 6 | namespace PFire.Core.Protocol.Messages.Inbound 7 | { 8 | internal sealed class GameInformation : XFireMessage 9 | { 10 | public GameInformation() : base(XFireMessageType.GameInformation) {} 11 | 12 | [XMessageField("gameid")] 13 | public int GameId { get; set; } 14 | 15 | [XMessageField("gip")] 16 | public int GameIP { get; set; } 17 | 18 | [XMessageField("gport")] 19 | public int GamePort { get; set; } 20 | 21 | public override async Task Process(IXFireClient context) 22 | { 23 | context.User.Game.Id = GameId; 24 | context.User.Game.Ip = GameIP; 25 | context.User.Game.Port = GamePort; 26 | await SendGameInfoToFriends(context); 27 | } 28 | public async Task SendGameInfoToFriends(IXFireClient context) 29 | { 30 | var friends = await context.Server.Database.QueryFriends(context.User); 31 | foreach (var friend in friends) 32 | { 33 | var otherSession = context.Server.GetSession(friend); 34 | if (otherSession != null) 35 | { 36 | await otherSession.SendAndProcessMessage(new FriendsGamesInfo(context.User)); 37 | } 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/PFire.Console/PFire.Console.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net10.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | PreserveNewest 15 | 16 | 17 | PreserveNewest 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | Always 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/Messages/Inbound/LoginRequest.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using PFire.Core.Protocol.Messages.Outbound; 3 | using PFire.Core.Session; 4 | 5 | namespace PFire.Core.Protocol.Messages.Inbound 6 | { 7 | internal sealed class LoginRequest : XFireMessage 8 | { 9 | public LoginRequest() : base(XFireMessageType.LoginRequest) {} 10 | 11 | [XMessageField("name")] 12 | public string Username { get; set; } 13 | 14 | [XMessageField("password")] 15 | public string Password { get; set; } 16 | 17 | [XMessageField("flags")] 18 | public int Flags { get; set; } 19 | 20 | public override async Task Process(IXFireClient context) 21 | { 22 | var user = await context.Server.Database.QueryUser(Username); 23 | if (user != null) 24 | { 25 | if (!BCrypt.Net.BCrypt.Verify(Password, user.Password)) 26 | { 27 | await context.SendAndProcessMessage(new LoginFailure()); 28 | return; 29 | } 30 | } 31 | else 32 | { 33 | var hashPassword = BCrypt.Net.BCrypt.HashPassword(Password); 34 | user = await context.Server.Database.InsertUser(Username, hashPassword, context.Salt); 35 | } 36 | 37 | await context.StartSession(user); 38 | 39 | var success = new LoginSuccess(); 40 | await context.SendAndProcessMessage(success); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/Messages/Outbound/FriendsGameInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Net; 6 | using System.Threading.Tasks; 7 | using Microsoft.Extensions.Logging; 8 | using PFire.Core.Models; 9 | using PFire.Core.Session; 10 | 11 | namespace PFire.Core.Protocol.Messages.Outbound 12 | { 13 | internal sealed class FriendsGamesInfo : XFireMessage 14 | { 15 | private readonly UserModel _ownerUser; 16 | 17 | public FriendsGamesInfo(UserModel owner) : base(XFireMessageType.FriendsGameInfo) 18 | { 19 | _ownerUser = owner; 20 | SessionIds = new List(); 21 | GameID = new List(); 22 | GameIP = new List(); 23 | GamePort = new List(); 24 | } 25 | 26 | [XMessageField("sid")] 27 | public List SessionIds { get; set; } 28 | 29 | [XMessageField("gameid")] 30 | public List GameID { get; set; } 31 | 32 | [XMessageField("gip")] 33 | public List GameIP { get; set; } 34 | 35 | [XMessageField("gport")] 36 | public List GamePort { get; set; } 37 | 38 | public override Task Process(IXFireClient client) 39 | { 40 | SessionIds.Add(client.Server.GetSession(_ownerUser).SessionId); 41 | GameID.Add(_ownerUser.Game.Id); 42 | GameIP.Add(_ownerUser.Game.Ip); 43 | GamePort.Add(_ownerUser.Game.Port); 44 | return Task.CompletedTask; 45 | } 46 | 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/XFireAttributes/ListAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | 5 | namespace PFire.Core.Protocol.XFireAttributes 6 | { 7 | public class ListAttribute : XFireAttribute 8 | { 9 | public override byte AttributeTypeId => 0x04; 10 | 11 | public override dynamic ReadValue(BinaryReader reader) 12 | { 13 | var listItemType = reader.ReadByte(); 14 | var listLength = reader.ReadInt16(); 15 | var itemAttribute = XFireAttributeFactory.Instance.GetAttribute(listItemType); 16 | 17 | var values = new List(); 18 | for(var i = 0; i < listLength; i++) 19 | { 20 | values.Add(itemAttribute.ReadValue(reader)); 21 | } 22 | 23 | return values; 24 | } 25 | 26 | public override void WriteValue(BinaryWriter writer, dynamic data) 27 | { 28 | var listLength = (short)data.Count; 29 | var dataType = data.GetType(); 30 | var itemType = dataType.GetGenericArguments()[0]; 31 | var attribute = XFireAttributeFactory.Instance.GetAttribute(itemType); 32 | 33 | attribute.WriteType(writer); 34 | writer.Write(listLength); 35 | 36 | for(var i = 0; i < listLength; i++) 37 | { 38 | attribute.WriteValue(writer, data[i]); 39 | } 40 | } 41 | 42 | // TODO: Refactor must be able to generic 43 | public override Type AttributeType => typeof(List<>); 44 | } 45 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PFire 2 | Emulated XFire server (Client 1.127) 3 | 4 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) 5 | [![Discord](https://img.shields.io/discord/619547253702393856.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/jWPWZu8DPy) 6 | ![.NET Core](https://github.com/darcymiranda/PFire/workflows/.NET%20Core/badge.svg) 7 | 8 | ## Requirements 9 | * [.Net 10 SDK](https://dotnet.microsoft.com/download) 10 | * [XFire Client v1.127 (Dropbox)](https://www.dropbox.com/s/fjj5u0uksg6t46f/Xfire.rar?dl=0) 11 | > _Note: If you don't trust the Dropbox link you can google for a copy of the installer._) 12 | 13 | ## Build 14 | 1. `cd src\PFire.Console` 15 | 2. `dotnet build` 16 | 17 | ## Run & Connect 18 | 1. Add `cs.xfire.com` to your hosts file with your IP 19 | * On Windows it's located at `C:\Windows\System32\drivers\etc\hosts` 20 | * `127.0.0.1 cs.xfire.com` 21 | > _Note: This is a workaround to redirect the XFire client to point to your localhost address (127.0.0.1) instead of the real server_ 22 | 23 | 2. Open a command line and run the following commands from the root directory 24 | * `cd src\PFire.Console` 25 | * `dotnet run` 26 | 27 | 3. Login with the XFire client (only tested with v1.127) and an account will be created automatically if it doesn't exist 28 | 29 | ## Test 30 | 1. `cd tests\PFire.Tests` 31 | 2. `dotnet test` 32 | 33 | ## Working Features 34 | * Friend search 35 | * Friend requests 36 | * Statuses 37 | * 1 to 1 chat messaging 38 | 39 | ### Screenshots 40 | ![Screenshot of XFire connecting to PFire](readme-screenshot.png) 41 | -------------------------------------------------------------------------------- /src/PFire.Infrastructure/Entities/Friend.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 3 | 4 | namespace PFire.Infrastructure.Entities 5 | { 6 | public class Friend : Entity 7 | { 8 | public int MeId { get; set; } 9 | public User Me { get; set; } 10 | 11 | public int ThemId { get; set; } 12 | public User Them { get; set; } 13 | 14 | public string Message { get; set; } 15 | 16 | public bool Pending { get; set; } 17 | } 18 | 19 | internal class FriendConfiguration : EntityConfiguration 20 | { 21 | protected override void ConfigureEntity(EntityTypeBuilder builder) 22 | { 23 | builder.ToTable(nameof(Friend)); 24 | 25 | builder.HasKey(x => new 26 | { 27 | RequesterId = x.MeId, 28 | RecipientId = x.ThemId 29 | }); 30 | 31 | builder.HasIndex(x => new 32 | { 33 | x.MeId, 34 | x.ThemId 35 | }) 36 | .IsUnique(); 37 | 38 | builder.HasOne(x => x.Me) 39 | .WithMany(x => x.MyFriends) 40 | .HasForeignKey(x => x.MeId) 41 | .OnDelete(DeleteBehavior.Restrict); 42 | 43 | builder.HasOne(x => x.Them) 44 | .WithMany(x => x.FriendsOf) 45 | .HasForeignKey(x => x.ThemId) 46 | .OnDelete(DeleteBehavior.Restrict); 47 | 48 | builder.Property(x => x.Message).HasMaxLength(1000); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/Messages/XFireMessageType.cs: -------------------------------------------------------------------------------- 1 | namespace PFire.Core.Protocol.Messages 2 | { 3 | public enum XFireMessageType : short 4 | { 5 | ChatContent = 0, 6 | LoginRequest = 1, 7 | UDPChatMessage = 2, 8 | ClientVersion = 3, 9 | GameInformation = 4, 10 | FriendRequest = 6, 11 | FriendRequestAccept = 7, 12 | FriendRequestDecline = 8, 13 | FriendRemoval = 9, 14 | ClientPreferencesUpdate = 10, 15 | UserLookup = 12, 16 | KeepAlive = 13, 17 | NicknameChange = 14, 18 | ClientConfiguration = 16, 19 | ConnectionInformation = 17, 20 | GameServerFetchFriendsFavorites = 21, 21 | GameServerFetchAll = 22, 22 | GroupCreate = 26, 23 | GroupRemove = 27, 24 | GroupRename = 28, 25 | StatusChange = 32, 26 | Unknown37 = 37, 27 | Logout = 36, 28 | LoginChallenge = 128, 29 | LoginFailure = 129, 30 | LoginSuccess = 130, 31 | FriendsList = 131, 32 | FriendsSessionAssign = 132, 33 | ServerChatMessage = 133, 34 | FriendsGameInfo = 135, 35 | FriendInvite = 138, 36 | FriendRemoved = 139, 37 | ClientPreferences = 141, 38 | UserLookupResult = 143, 39 | ServerPong = 144, 40 | ServerList = 148, 41 | GameServerSendFriendsFavorites = 149, 42 | GameServerSendAll = 150, 43 | Groups = 151, 44 | GroupsFriends = 152, 45 | GroupCreateConfirmation = 153, 46 | FriendStatusChange = 154, 47 | ChatRooms = 155, 48 | SystemBroadcast = 169, 49 | 50 | Did = 400 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/Messages/Inbound/FriendRequestAccept.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading.Tasks; 3 | using PFire.Core.Protocol.Messages.Outbound; 4 | using PFire.Core.Session; 5 | 6 | namespace PFire.Core.Protocol.Messages.Inbound 7 | { 8 | internal sealed class FriendRequestAccept : XFireMessage 9 | { 10 | public FriendRequestAccept() : base(XFireMessageType.FriendRequestAccept) {} 11 | 12 | [XMessageField("name")] 13 | public string FriendUsername { get; set; } 14 | 15 | public override async Task Process(IXFireClient context) 16 | { 17 | var friend = await context.Server.Database.QueryUser(FriendUsername); 18 | 19 | await context.Server.Database.InsertMutualFriend(context.User, friend); 20 | 21 | await context.SendAndProcessMessage(new FriendsList(context.User)); 22 | await context.SendAndProcessMessage(new FriendsSessionAssign(context.User)); 23 | 24 | // It's possible to accept a friend request where the inviter is not online 25 | var friendSession = context.Server.GetSession(friend); 26 | if (friendSession != null) 27 | { 28 | await friendSession.SendAndProcessMessage(new FriendsList(friend)); 29 | await friendSession.SendAndProcessMessage(new FriendsSessionAssign(friend)); 30 | } 31 | 32 | var pendingRequests = await context.Server.Database.QueryPendingFriendRequests(context.User); 33 | var pq = pendingRequests.Where(a => a.Id == friend.Id).ToArray(); 34 | await context.Server.Database.DeletePendingFriendRequest(context.User, pq); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/Messages/Outbound/UserLookupResult.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using PFire.Core.Session; 5 | 6 | namespace PFire.Core.Protocol.Messages.Outbound 7 | { 8 | internal sealed class UserLookupResult : XFireMessage 9 | { 10 | private readonly string _queryByUsername; 11 | 12 | public UserLookupResult(string username) : base(XFireMessageType.UserLookupResult) 13 | { 14 | _queryByUsername = username; 15 | 16 | Usernames = new List(); 17 | FirstNames = new List(); 18 | LastNames = new List(); 19 | Emails = new List(); 20 | } 21 | 22 | [XMessageField("name")] 23 | public List Usernames { get; } 24 | 25 | [XMessageField("fname")] 26 | public List FirstNames { get; } 27 | 28 | [XMessageField("lname")] 29 | public List LastNames { get; } 30 | 31 | [XMessageField("email")] 32 | public List Emails { get; } 33 | 34 | public override async Task Process(IXFireClient context) 35 | { 36 | var queryUsers = await context.Server.Database.QueryUsers(_queryByUsername); 37 | var usernames = queryUsers.Select(a => a.Username).ToList(); 38 | 39 | Usernames.AddRange(usernames); 40 | 41 | // Don't really care about these but they're necessary to work properly 42 | var unknowns = usernames.Select(a => "Unknown").ToList(); 43 | 44 | FirstNames.AddRange(unknowns); 45 | LastNames.AddRange(unknowns); 46 | Emails.AddRange(unknowns); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/PFire.Core/TcpServer.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Sockets; 2 | using System.Threading.Tasks; 3 | using Microsoft.Extensions.Logging; 4 | using PFire.Core.Session; 5 | 6 | namespace PFire.Core 7 | { 8 | internal sealed class TcpServer : ITcpServer 9 | { 10 | private readonly IXFireClientManager _clientManager; 11 | private readonly TcpListener _listener; 12 | private readonly ILogger _logger; 13 | private bool _running; 14 | 15 | public TcpServer(TcpListener listener, IXFireClientManager clientManager, ILogger logger) 16 | { 17 | _listener = listener; 18 | _clientManager = clientManager; 19 | _logger = logger; 20 | } 21 | 22 | public event ITcpServer.OnReceiveHandler OnReceive; 23 | public event ITcpServer.OnConnectionHandler OnConnection; 24 | public event ITcpServer.OnDisconnectionHandler OnDisconnection; 25 | 26 | public async Task Listen() 27 | { 28 | _running = true; 29 | _listener.Start(); 30 | _logger.LogInformation($"PFire Server listening on {_listener.LocalEndpoint}"); 31 | await Accept().ConfigureAwait(false); 32 | } 33 | 34 | public void Shutdown() 35 | { 36 | _listener.Stop(); 37 | _running = false; 38 | } 39 | 40 | private async Task Accept() 41 | { 42 | while (_running) 43 | { 44 | var tcpClient = await _listener.AcceptTcpClientAsync().ConfigureAwait(false); 45 | var newXFireClient = new XFireClient(tcpClient, _clientManager, _logger, OnReceive, OnDisconnection); 46 | 47 | OnConnection?.Invoke(newXFireClient); 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/Messages/Outbound/FriendsSessionAssign.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using PFire.Core.Models; 6 | using PFire.Core.Session; 7 | 8 | namespace PFire.Core.Protocol.Messages.Outbound 9 | { 10 | internal sealed class FriendsSessionAssign : XFireMessage 11 | { 12 | private static readonly Guid FriendIsOffLineSessionId = Guid.Empty; 13 | private readonly UserModel _ownerUser; 14 | 15 | public FriendsSessionAssign(UserModel owner) : base(XFireMessageType.FriendsSessionAssign) 16 | { 17 | _ownerUser = owner; 18 | UserIds = new List(); 19 | SessionIds = new List(); 20 | } 21 | 22 | [XMessageField("userid")] 23 | public List UserIds { get; } 24 | 25 | [XMessageField("sid")] 26 | public List SessionIds { get; } 27 | 28 | [XMessageField(0x0b)] 29 | public byte Unknown { get; set; } 30 | 31 | public static FriendsSessionAssign UserWentOffline(UserModel user) 32 | { 33 | return new FriendsSessionAssign(user) {UserIds = {user.Id}, SessionIds = {FriendIsOffLineSessionId}}; 34 | } 35 | 36 | public static FriendsSessionAssign UserCameOnline(UserModel user, Guid sessionId) 37 | { 38 | return new FriendsSessionAssign(user) {UserIds = {user.Id}, SessionIds = {sessionId}}; 39 | } 40 | 41 | public override async Task Process(IXFireClient client) 42 | { 43 | var friends = (await client.Server.Database.QueryFriends(_ownerUser)) 44 | .Select(x => new {User = x, Session = client.Server.GetSession(x)}) 45 | .Where(x => x.Session != null); 46 | 47 | foreach (var friend in friends) 48 | { 49 | UserIds.Add(friend.User.Id); 50 | SessionIds.Add(friend.Session.SessionId); 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/Messages/Outbound/GameServerSendFriendsFavorites.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Net; 6 | using System.Threading.Tasks; 7 | using PFire.Core.Session; 8 | 9 | namespace PFire.Core.Protocol.Messages.Outbound 10 | { 11 | internal sealed class GameServerSendFriendsFavorites : XFireMessage 12 | { 13 | public GameServerSendFriendsFavorites(int gameId) : base(XFireMessageType.GameServerSendFriendsFavorites) 14 | { 15 | GameId = gameId; 16 | GameIps = new List(); 17 | GamePorts = new List(); 18 | FriendIds = new List>(); 19 | } 20 | 21 | [XMessageField("gameid")] 22 | public int GameId { get; set; } 23 | 24 | [XMessageField("gip")] 25 | public List GameIps { get; set; } 26 | 27 | [XMessageField("gport")] 28 | public List GamePorts { get; set; } 29 | 30 | [XMessageField("friends")] 31 | public List> FriendIds { get; set; } 32 | 33 | public override Task Process(IXFireClient context) 34 | { 35 | //TODO: Have a Database of IPs and Ports, with a user id who favorited it that is fetched by gameid 36 | // Probably reads from the favorites database based on userid and gameid. 37 | // 38 | // You will need to start off processing with making a new temporary List to put inside of FriendIds to satisfy the nested list need, this will hold the userid who favorited the server. 39 | // Start off with sending back the GameId sent 40 | // Iterate that list of favorites into Ips and Ports (unsigned ints on both) and your new userid List 41 | // If no hits, send GameIps and GamePorts with empty Lists, for FriendIds send back an empty List> regardless, because the client expects a response. 42 | 43 | return Task.CompletedTask; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/PFire.Infrastructure/Services/DatabaseContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Microsoft.EntityFrameworkCore; 5 | using PFire.Infrastructure.Entities; 6 | 7 | namespace PFire.Infrastructure.Services 8 | { 9 | public interface IDatabaseMigrator 10 | { 11 | Task Migrate(); 12 | } 13 | 14 | public interface IDatabaseContext 15 | { 16 | DbSet Set() where T : Entity; 17 | Task SaveChanges(); 18 | } 19 | 20 | internal class DatabaseContext : DbContext, IDatabaseContext, IDatabaseMigrator 21 | { 22 | public DatabaseContext(DbContextOptions options) : base(options) {} 23 | 24 | DbSet IDatabaseContext.Set() 25 | { 26 | return Set(); 27 | } 28 | 29 | Task IDatabaseContext.SaveChanges() 30 | { 31 | var entries = ChangeTracker.Entries() 32 | .Where(e => e.Entity is Entity && (e.State == EntityState.Added || e.State == EntityState.Modified)); 33 | 34 | foreach(var entry in entries) 35 | { 36 | var entity = (Entity)entry.Entity; 37 | 38 | if (entry.State == EntityState.Added) 39 | { 40 | entity.DateCreated = DateTime.UtcNow; 41 | } 42 | else if (entry.State == EntityState.Modified) 43 | { 44 | entity.DateModified = DateTime.UtcNow; 45 | } 46 | } 47 | 48 | return SaveChangesAsync(); 49 | } 50 | 51 | public Task Migrate() 52 | { 53 | return Database.MigrateAsync(); 54 | } 55 | 56 | protected override void OnModelCreating(ModelBuilder modelBuilder) 57 | { 58 | modelBuilder.ApplyConfiguration(new FriendConfiguration()); 59 | modelBuilder.ApplyConfiguration(new GroupConfiguration()); 60 | modelBuilder.ApplyConfiguration(new UserConfiguration()); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/PFire.Core/Session/XFireClientManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Linq; 4 | using Microsoft.Extensions.Logging; 5 | using PFire.Core.Models; 6 | 7 | namespace PFire.Core.Session 8 | { 9 | internal interface IXFireClientManager 10 | { 11 | IXFireClient GetSession(Guid sessionId); 12 | IXFireClient GetSession(UserModel user); 13 | void AddSession(IXFireClient session); 14 | void RemoveSession(IXFireClient session); 15 | } 16 | 17 | internal sealed class XFireClientManager : IXFireClientManager 18 | { 19 | private readonly ConcurrentDictionary _sessions; 20 | 21 | public XFireClientManager() 22 | { 23 | _sessions = new ConcurrentDictionary(); 24 | } 25 | 26 | public void AddSession(IXFireClient session) 27 | { 28 | if (!_sessions.TryAdd(session.SessionId, session)) 29 | { 30 | session.Logger.LogWarning($"Tried to add a user with session id {session.SessionId} that already existed."); 31 | } 32 | } 33 | 34 | public IXFireClient GetSession(Guid sessionId) 35 | { 36 | return _sessions.TryGetValue(sessionId, out var result) ? result : null; 37 | } 38 | 39 | public IXFireClient GetSession(UserModel user) 40 | { 41 | var session = _sessions.ToList().Select(x => x.Value).FirstOrDefault(a => a.User?.Id == user.Id); 42 | 43 | return session == null ? null : GetSession(session.SessionId); 44 | } 45 | 46 | public void RemoveSession(IXFireClient session) 47 | { 48 | RemoveSession(session.SessionId); 49 | } 50 | 51 | public void RemoveSession(Guid sessionId) 52 | { 53 | if (!_sessions.TryRemove(sessionId, out var currentSession)) 54 | { 55 | return; 56 | } 57 | 58 | currentSession.Disconnect(); 59 | currentSession.Dispose(); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/PFire.Infrastructure/Migrations/20250120173254_CreateGroup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | #nullable disable 5 | 6 | namespace PFire.Infrastructure.Migrations 7 | { 8 | /// 9 | public partial class CreateGroup : Migration 10 | { 11 | /// 12 | protected override void Up(MigrationBuilder migrationBuilder) 13 | { 14 | migrationBuilder.CreateTable( 15 | name: "Group", 16 | columns: table => new 17 | { 18 | Id = table.Column(type: "INTEGER", nullable: false) 19 | .Annotation("Sqlite:Autoincrement", true), 20 | Name = table.Column(type: "TEXT", maxLength: 1000, nullable: false), 21 | OwnerId = table.Column(type: "INTEGER", nullable: false), 22 | Version = table.Column(type: "BLOB", rowVersion: true, nullable: true), 23 | DateCreated = table.Column(type: "TEXT", nullable: false), 24 | DateModified = table.Column(type: "TEXT", nullable: false) 25 | }, 26 | constraints: table => 27 | { 28 | table.PrimaryKey("PK_Group", x => x.Id); 29 | table.ForeignKey( 30 | name: "FK_Group_User_OwnerId", 31 | column: x => x.OwnerId, 32 | principalTable: "User", 33 | principalColumn: "Id", 34 | onDelete: ReferentialAction.Cascade); 35 | }); 36 | 37 | migrationBuilder.CreateIndex( 38 | name: "IX_Group_OwnerId", 39 | table: "Group", 40 | column: "OwnerId"); 41 | } 42 | 43 | /// 44 | protected override void Down(MigrationBuilder migrationBuilder) 45 | { 46 | migrationBuilder.DropTable( 47 | name: "Group"); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/XFireAttributes/MessageAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Text; 5 | using PFire.Core.Protocol.Messages; 6 | using PFire.Core.Protocol.Messages.Bidirectional; 7 | 8 | namespace PFire.Core.Protocol.XFireAttributes 9 | { 10 | public class MessageAttribute : XFireAttribute 11 | { 12 | private static readonly Dictionary MESSAGE_TYPES = new Dictionary 13 | { 14 | { 0, new ChatMessage() }, 15 | { 1, new ChatAcknowledgement() } 16 | }; 17 | 18 | private IMessage CreateMessage(short type) 19 | { 20 | if(!MESSAGE_TYPES.ContainsKey(type)) 21 | { 22 | throw new UnknownMessageTypeException((XFireMessageType)type); 23 | } 24 | 25 | return (IMessage)Activator.CreateInstance(MESSAGE_TYPES[type].GetType()); 26 | } 27 | 28 | public override Type AttributeType => typeof(Dictionary); 29 | 30 | public override byte AttributeTypeId => 0x15; 31 | 32 | public override dynamic ReadValue(BinaryReader reader) 33 | { 34 | var values = new Dictionary(); 35 | var mapLength = reader.ReadByte(); 36 | 37 | for(var i = 0; i < mapLength; i++) 38 | { 39 | var messageTypeName = ReadInt8String(reader); 40 | var messageType = XFireAttributeFactory.Instance.GetAttribute(reader.ReadByte()).ReadValue(reader); 41 | var message = MessageSerializer.Deserialize(reader, CreateMessage(messageType)); 42 | values.Add(messageTypeName, message); 43 | } 44 | 45 | return values; 46 | } 47 | 48 | public override void WriteValue(BinaryWriter writer, dynamic data) 49 | { 50 | } 51 | 52 | private string ReadInt8String(BinaryReader reader) 53 | { 54 | var length = reader.ReadByte(); 55 | return Encoding.UTF8.GetString(reader.ReadBytes(length)); 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/Messages/Inbound/ConnectionInformation.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading.Tasks; 3 | using PFire.Core.Protocol.Messages.Outbound; 4 | using PFire.Core.Session; 5 | 6 | namespace PFire.Core.Protocol.Messages.Inbound 7 | { 8 | internal sealed class ConnectionInformation : XFireMessage 9 | { 10 | public ConnectionInformation() : base(XFireMessageType.ConnectionInformation) {} 11 | 12 | [XMessageField("conn")] 13 | public int Connection { get; set; } 14 | 15 | [XMessageField("nat")] 16 | public int Nat { get; set; } 17 | 18 | [XMessageField("naterr")] 19 | public int NatError { get; set; } 20 | 21 | [XMessageField("sec")] 22 | public int Sec { get; set; } 23 | 24 | [XMessageField("clientip")] 25 | public int ClientIp { get; set; } 26 | 27 | [XMessageField("upnpinfo")] 28 | public string UpnpInfo { get; set; } 29 | 30 | public override async Task Process(IXFireClient context) 31 | { 32 | var clientPrefs = new ClientPreferences(); 33 | await context.SendAndProcessMessage(clientPrefs); 34 | 35 | var groups = new Groups(); 36 | await context.SendAndProcessMessage(groups); 37 | 38 | var groupsFriends = new GroupsFriends(); 39 | await context.SendAndProcessMessage(groupsFriends); 40 | 41 | var serverList = new ServerList(); 42 | await context.SendAndProcessMessage(serverList); 43 | 44 | var chatRooms = new ChatRooms(); 45 | await context.SendAndProcessMessage(chatRooms); 46 | 47 | // TODO: Remove chat room mode 48 | var otherUsers = await context.Server.Database.AddEveryoneAsFriends(context.User); 49 | 50 | var friendsList = new FriendsList(context.User); 51 | await context.SendAndProcessMessage(friendsList); 52 | 53 | var friendsStatus = new FriendsSessionAssign(context.User); 54 | await context.SendAndProcessMessage(friendsStatus); 55 | 56 | // TODO: Remove chat room mode 57 | foreach (var otherUser in otherUsers) 58 | { 59 | var otherSession = context.Server.GetSession(otherUser); 60 | if (otherSession != null) 61 | { 62 | await otherSession.SendAndProcessMessage(new FriendsList(otherSession.User)); 63 | await otherSession.SendMessage( 64 | FriendsSessionAssign.UserCameOnline(context.User, context.SessionId)); 65 | } 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /src/PFire.Infrastructure/Migrations/20250202021012_ModifyAuditEntities.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | #nullable disable 5 | 6 | namespace PFire.Infrastructure.Migrations 7 | { 8 | /// 9 | public partial class ModifyAuditEntities : Migration 10 | { 11 | /// 12 | protected override void Up(MigrationBuilder migrationBuilder) 13 | { 14 | migrationBuilder.AlterColumn( 15 | name: "DateModified", 16 | table: "User", 17 | type: "TEXT", 18 | nullable: true, 19 | oldClrType: typeof(DateTime), 20 | oldType: "TEXT"); 21 | 22 | migrationBuilder.AlterColumn( 23 | name: "DateModified", 24 | table: "Group", 25 | type: "TEXT", 26 | nullable: true, 27 | oldClrType: typeof(DateTime), 28 | oldType: "TEXT"); 29 | 30 | migrationBuilder.AlterColumn( 31 | name: "DateModified", 32 | table: "Friend", 33 | type: "TEXT", 34 | nullable: true, 35 | oldClrType: typeof(DateTime), 36 | oldType: "TEXT"); 37 | } 38 | 39 | /// 40 | protected override void Down(MigrationBuilder migrationBuilder) 41 | { 42 | migrationBuilder.AlterColumn( 43 | name: "DateModified", 44 | table: "User", 45 | type: "TEXT", 46 | nullable: false, 47 | defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), 48 | oldClrType: typeof(DateTime), 49 | oldType: "TEXT", 50 | oldNullable: true); 51 | 52 | migrationBuilder.AlterColumn( 53 | name: "DateModified", 54 | table: "Group", 55 | type: "TEXT", 56 | nullable: false, 57 | defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), 58 | oldClrType: typeof(DateTime), 59 | oldType: "TEXT", 60 | oldNullable: true); 61 | 62 | migrationBuilder.AlterColumn( 63 | name: "DateModified", 64 | table: "Friend", 65 | type: "TEXT", 66 | nullable: false, 67 | defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), 68 | oldClrType: typeof(DateTime), 69 | oldType: "TEXT", 70 | oldNullable: true); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/XFireAttributeFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using PFire.Core.Protocol.XFireAttributes; 5 | 6 | namespace PFire.Core.Protocol 7 | { 8 | public class XFireAttributeFactory 9 | { 10 | private static readonly XFireAttributeFactory instance = null; 11 | 12 | private readonly Dictionary _attributeTypes = new Dictionary(); 13 | 14 | private static readonly byte[] IgnoreKnownUnimplementedTypes = {18}; 15 | 16 | private XFireAttributeFactory() 17 | { 18 | Add(new StringAttribute()); 19 | Add(new Int32Attribute()); 20 | Add(new SessionIdAttribute()); 21 | Add(new ListAttribute()); 22 | Add(new DidAttribute()); 23 | Add(new Int8KeyMapAttribute()); 24 | Add(new StringKeyMapAttribute()); 25 | Add(new Int8Attribute()); 26 | Add(new MessageAttribute()); 27 | Add(new NullAttribute()); 28 | } 29 | 30 | private void Add(XFireAttribute attributeValue) 31 | { 32 | _attributeTypes.Add(attributeValue.AttributeTypeId, attributeValue); 33 | } 34 | 35 | public XFireAttribute GetAttribute(byte type) 36 | { 37 | if (_attributeTypes.TryGetValue(type, out var xFireAttribute)) 38 | { 39 | return xFireAttribute; 40 | } 41 | 42 | // Avoid having to implement these attribute types to continue processing other requests instead of 43 | // throwing an exception 44 | if (IgnoreKnownUnimplementedTypes.Contains(type)) 45 | { 46 | return new NullAttribute(); 47 | } 48 | 49 | throw new UnknownXFireAttributeTypeException(type); 50 | } 51 | 52 | public XFireAttribute GetAttribute(Type type) 53 | { 54 | foreach (var keyValuePair in _attributeTypes.Where(x => x.Value.AttributeType.Name == type.Name)) 55 | { 56 | // Need to match on the first generic type for maps/dictionaries 57 | if (type.GenericTypeArguments.Length > 1) 58 | { 59 | if (type.GenericTypeArguments.FirstOrDefault() != 60 | keyValuePair.Value.AttributeType.GenericTypeArguments.FirstOrDefault()) 61 | { 62 | continue; 63 | } 64 | } 65 | 66 | return GetAttribute(keyValuePair.Value.AttributeTypeId); 67 | } 68 | 69 | throw new KeyNotFoundException($"XFireAttribute with type of {type.Name} not found"); 70 | } 71 | 72 | public static XFireAttributeFactory Instance => instance ?? new XFireAttributeFactory(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/XFireMessageTypeFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using PFire.Core.Protocol.Messages; 4 | using PFire.Core.Protocol.Messages.Bidirectional; 5 | using PFire.Core.Protocol.Messages.Inbound; 6 | using PFire.Core.Protocol.Messages.Outbound; 7 | 8 | namespace PFire.Core.Protocol 9 | { 10 | public class XFireMessageTypeFactory 11 | { 12 | private static readonly XFireMessageTypeFactory instance = null; 13 | 14 | private readonly Dictionary _messages = new Dictionary(); 15 | 16 | private XFireMessageTypeFactory() 17 | { 18 | Add(new ClientVersion()); 19 | Add(new LoginRequest()); 20 | Add(new LoginFailure()); 21 | Add(new LoginSuccess()); 22 | Add(new ClientConfiguration()); 23 | Add(new ClientPreferencesUpdate()); 24 | Add(new ConnectionInformation()); 25 | Add(new Groups()); 26 | Add(new GroupsFriends()); 27 | Add(new GroupCreate()); 28 | Add(new GroupRemove()); 29 | Add(new GroupRename()); 30 | Add(new ServerList()); 31 | Add(new ChatRooms()); 32 | Add(new GameInformation()); 33 | Add(new KeepAlive()); 34 | Add(new Did()); 35 | Add(new ChatMessage()); 36 | Add(new UserLookup()); 37 | Add(new FriendRequest()); 38 | Add(new FriendRequestAccept()); 39 | Add(new FriendRequestDecline()); 40 | Add(new FriendRemoval()); 41 | Add(new GameServerFetchAll()); 42 | Add(new GameServerFetchFriendsFavorites()); 43 | Add(new NicknameChange()); 44 | Add(new StatusChange()); 45 | Add(new Logout()); 46 | } 47 | 48 | private void Add(IMessage message) 49 | { 50 | _messages.Add(message.MessageTypeId, message); 51 | } 52 | 53 | public Type GetMessageType(XFireMessageType messageType) 54 | { 55 | // Hack: Client sends message type of 2 for chat messages but expects message type of 133 on receive... 56 | // this is because the client to client message (type 2) is send via UDP to the clients directly, 57 | // whereas 133 is a message routed via the server to the client 58 | if(messageType == XFireMessageType.UDPChatMessage) 59 | { 60 | return _messages[XFireMessageType.ServerChatMessage].GetType(); 61 | } 62 | 63 | if(!_messages.TryGetValue(messageType, out var message)) 64 | { 65 | throw new UnknownMessageTypeException(messageType); 66 | } 67 | 68 | return message.GetType(); 69 | } 70 | 71 | public static XFireMessageTypeFactory Instance => instance ?? new XFireMessageTypeFactory(); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/Messages/Outbound/LoginSuccess.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Threading.Tasks; 4 | using Microsoft.Extensions.Logging; 5 | using PFire.Core.Session; 6 | 7 | namespace PFire.Core.Protocol.Messages.Outbound 8 | { 9 | internal sealed class LoginSuccess : XFireMessage 10 | { 11 | public LoginSuccess() : base(XFireMessageType.LoginSuccess) {} 12 | 13 | [XMessageField("userid")] 14 | public int UserId { get; set; } 15 | 16 | [XMessageField("sid")] 17 | public Guid SessionId { get; set; } 18 | 19 | [XMessageField("nick")] 20 | public string Nickname { get; set; } 21 | 22 | [XMessageField("status")] 23 | public int Status { get; set; } 24 | 25 | [XMessageField("dlset")] 26 | public string DlSet { get; set; } 27 | 28 | [XMessageField("p2pset")] 29 | public string P2PSet { get; set; } 30 | 31 | [XMessageField("clntset")] 32 | public string ClientSet { get; set; } 33 | 34 | [XMessageField("minrect")] 35 | public int MinRect { get; set; } 36 | 37 | [XMessageField("maxrect")] 38 | public int MaxRect { get; set; } 39 | 40 | [XMessageField("ctry")] 41 | public int Country { get; set; } 42 | 43 | [XMessageField("n1")] 44 | public int N1 { get; set; } 45 | 46 | [XMessageField("n2")] 47 | public int N2 { get; set; } 48 | 49 | [XMessageField("n3")] 50 | public int N3 { get; set; } 51 | 52 | [XMessageField("pip")] 53 | public int PublicIp { get; set; } 54 | 55 | [XMessageField("salt")] 56 | public string Salt { get; set; } 57 | 58 | [XMessageField("reason")] 59 | public string Reason { get; set; } 60 | 61 | public override Task Process(IXFireClient context) 62 | { 63 | UserId = context.User.Id; 64 | SessionId = context.SessionId; 65 | Status = 0; 66 | Nickname = string.IsNullOrEmpty(context.User.Nickname) ? context.User.Username : context.User.Nickname; 67 | MinRect = 1; 68 | MaxRect = 164867; 69 | var ipAddress = StripPortFromIpAddress(context.RemoteEndPoint.ToString()); 70 | PublicIp = BitConverter.ToInt32(IPAddress.Parse(ipAddress).GetAddressBytes(), 0); 71 | Salt = context.Salt; 72 | Reason = "Mq_P8Ad3aMEUvFinw0ceu6FITnZTWXxg46XU8xHW"; 73 | 74 | context.Logger.LogDebug($"User {context.User.Username}[{context.User.Id}] logged in successfully with session id {context.SessionId}"); 75 | context.Logger.LogInformation($"User {context.User.Username} logged in"); 76 | 77 | return Task.CompletedTask; 78 | } 79 | 80 | private static string StripPortFromIpAddress(string address) 81 | { 82 | return address.Substring(0, address.IndexOf(":", StringComparison.Ordinal)); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/XFireAttributes/MapAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Text; 5 | 6 | namespace PFire.Core.Protocol.XFireAttributes 7 | { 8 | public abstract class MapAttribute : XFireAttribute 9 | { 10 | public override Type AttributeType => typeof(Dictionary); 11 | 12 | public abstract override byte AttributeTypeId { get; } 13 | 14 | public override dynamic ReadValue(BinaryReader reader) 15 | { 16 | var values = new Dictionary(); 17 | var mapLength = reader.ReadByte(); 18 | var keyAttribute = XFireAttributeFactory.Instance.GetAttribute(typeof(T)); 19 | 20 | for(var i = 0; i < mapLength; i++) 21 | { 22 | // TODO: Fix hack 23 | // Stupid protocol decides to not be nice and expect an 8bit string length prefix instead of the normal 16 for string key mapped types 24 | 25 | var key = keyAttribute is StringAttribute ? ReadInt8StringLengthHack(reader) : keyAttribute.ReadValue(reader); 26 | 27 | var type = reader.ReadByte(); 28 | var attribute = XFireAttributeFactory.Instance.GetAttribute(type); 29 | 30 | values.Add(key, attribute.ReadValue(reader)); 31 | } 32 | 33 | return values; 34 | } 35 | 36 | public override void WriteValue(BinaryWriter writer, dynamic data) 37 | { 38 | var mapLength = (byte)data.Count; 39 | var keyAttribute = XFireAttributeFactory.Instance.GetAttribute(typeof(T)); 40 | 41 | writer.Write(mapLength); 42 | 43 | foreach (var pair in data) 44 | { 45 | var key = pair.Key; 46 | var value = pair.Value; 47 | var attribute = XFireAttributeFactory.Instance.GetAttribute(value.GetType()); 48 | 49 | // TODO: Fix hack 50 | // Stupid protocol decides to not be nice and expect an 8bit string length prefix instead of 16 for string key mapped types 51 | if (keyAttribute is StringAttribute) 52 | { 53 | WriteInt8StringLengthHack(writer, (string)key); 54 | } 55 | else 56 | { 57 | keyAttribute.WriteValue(writer, key); 58 | } 59 | 60 | attribute.WriteType(writer); 61 | attribute.WriteValue(writer, value); 62 | } 63 | } 64 | 65 | private void WriteInt8StringLengthHack(BinaryWriter writer, string value) 66 | { 67 | writer.Write((byte)value.Length); 68 | writer.Write(Encoding.UTF8.GetBytes(value)); 69 | } 70 | 71 | private string ReadInt8StringLengthHack(BinaryReader reader) 72 | { 73 | var length = reader.ReadByte(); 74 | return Encoding.UTF8.GetString(reader.ReadBytes(length)); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/Messages/Outbound/ClientPreferences.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using PFire.Core.Session; 4 | 5 | namespace PFire.Core.Protocol.Messages.Outbound 6 | { 7 | internal sealed class ClientPreferences : XFireMessage 8 | { 9 | public ClientPreferences() : base(XFireMessageType.ClientPreferences) {} 10 | 11 | [XMessageField(0x4c)] 12 | public Dictionary preferences { get; set; } 13 | 14 | public override async Task Process(IXFireClient context) 15 | { 16 | var clientPreferences = await context.Server.Database.GetClientPreferences(context.User); 17 | 18 | preferences = new Dictionary 19 | { 20 | { (int)Enums.ClientPreferences.GameStatusShowMyFriends, clientPreferences.GameStatusShowMyFriends ? "1": "0" }, 21 | { (int)Enums.ClientPreferences.GameStatusShowMyGameServer, clientPreferences.GameStatusShowMyGameServer ? "1": "0" }, 22 | { (int)Enums.ClientPreferences.GameStatusShowMyProfile, clientPreferences.GameStatusShowMyProfile ? "1": "0" }, 23 | { (int)Enums.ClientPreferences.PlaySoundSendOrReceiveMessage, clientPreferences.PlaySoundSendOrReceiveMessage ? "1": "0" }, 24 | { (int)Enums.ClientPreferences.PlaySoundReceiveMessageWhileGaming, clientPreferences.PlaySoundReceiveMessageWhileGaming ? "1": "0" }, 25 | { (int)Enums.ClientPreferences.ChatShowTimestamps, clientPreferences.ChatShowTimestamps ? "1": "0" }, 26 | { (int)Enums.ClientPreferences.PlaySoundFriendLogsOnOff, clientPreferences.PlaySoundFriendLogsOnOff ? "1": "0" }, 27 | { (int)Enums.ClientPreferences.GameStatusShowFriendOfFriends, clientPreferences.GameStatusShowFriendOfFriends ? "1": "0" }, 28 | { (int)Enums.ClientPreferences.ShowOfflineFriends, clientPreferences.ShowOfflineFriends ? "1": "0" }, 29 | { (int)Enums.ClientPreferences.ShowNicknames, clientPreferences.ShowNicknames ? "1": "0" }, 30 | { (int)Enums.ClientPreferences.ShowVoiceChatServerToFriends, clientPreferences.ShowVoiceChatServerToFriends ? "1": "0" }, 31 | { (int)Enums.ClientPreferences.ShowWhenTyping, clientPreferences.ShowWhenTyping ? "1": "0" }, 32 | { (int)Enums.ClientPreferences.NotificationFriendLogsOnOff, clientPreferences.NotificationFriendLogsOnOff ? "1": "0" }, 33 | { (int)Enums.ClientPreferences.NotificationDownloadStartsFinishes, clientPreferences.NotificationDownloadStartsFinishes ? "1": "0" }, 34 | { (int)Enums.ClientPreferences.PlaySoundSomeoneJoinsLeaveChatroom, clientPreferences.PlaySoundSomeoneJoinsLeaveChatroom ? "1": "0" }, 35 | { (int)Enums.ClientPreferences.PlaySoundSendReceiveVoiceChatRequest, clientPreferences.PlaySoundSendReceiveVoiceChatRequest ? "1": "0" }, 36 | { (int)Enums.ClientPreferences.PlaySoundScreenshotWhileGaming, clientPreferences.PlaySoundScreenshotWhileGaming ? "1": "0" }, 37 | { (int)Enums.ClientPreferences.NotificationConnectionStateChanges, clientPreferences.NotificationConnectionStateChanges ? "1": "0" } 38 | }; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/PFire.Core/PFireServer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using PFire.Core.Models; 4 | using PFire.Core.Protocol.Messages; 5 | using PFire.Core.Protocol.Messages.Outbound; 6 | using PFire.Core.Services; 7 | using PFire.Core.Session; 8 | 9 | namespace PFire.Core 10 | { 11 | public interface IPFireServer 12 | { 13 | Task Start(); 14 | Task Stop(); 15 | } 16 | 17 | internal sealed class PFireServer : IPFireServer 18 | { 19 | private readonly IXFireClientManager _clientManager; 20 | private readonly ITcpServer _server; 21 | 22 | public PFireServer(IPFireDatabase pFireDatabase, IXFireClientManager xFireClientManager, ITcpServer server) 23 | { 24 | Database = pFireDatabase; 25 | _clientManager = xFireClientManager; 26 | 27 | _server = server; 28 | _server.OnReceive += HandleRequest; 29 | _server.OnConnection += HandleNewConnection; 30 | _server.OnDisconnection += OnDisconnection; 31 | } 32 | 33 | public IPFireDatabase Database { get; } 34 | 35 | public async Task Start() 36 | { 37 | await _server.Listen(); 38 | } 39 | 40 | public Task Stop() 41 | { 42 | _server.Shutdown(); 43 | 44 | return Task.CompletedTask; 45 | } 46 | 47 | private async Task OnDisconnection(IXFireClient disconnectedClient) 48 | { 49 | // we have to remove the session first 50 | // because of the friends of this user processing 51 | RemoveSession(disconnectedClient); 52 | 53 | await UpdateFriendsWithDisconnetedStatus(disconnectedClient); 54 | } 55 | 56 | private async Task UpdateFriendsWithDisconnetedStatus(IXFireClient disconnectedClient) 57 | { 58 | var friends = await Database.QueryFriends(disconnectedClient.User); 59 | 60 | foreach (var friend in friends) 61 | { 62 | var friendClient = GetSession(friend); 63 | if (friendClient != null) 64 | { 65 | await friendClient.SendAndProcessMessage(new FriendsSessionAssign(friend)); 66 | } 67 | } 68 | } 69 | 70 | private Task HandleNewConnection(IXFireClient sessionContext) 71 | { 72 | AddSession(sessionContext); 73 | 74 | return Task.CompletedTask; 75 | } 76 | 77 | private async Task HandleRequest(IXFireClient context, IMessage message) 78 | { 79 | context.Server = this; 80 | await message.Process(context); 81 | } 82 | 83 | public IXFireClient GetSession(Guid sessionId) 84 | { 85 | return _clientManager.GetSession(sessionId); 86 | } 87 | 88 | public IXFireClient GetSession(UserModel user) 89 | { 90 | return _clientManager.GetSession(user); 91 | } 92 | 93 | private void AddSession(IXFireClient session) 94 | { 95 | _clientManager.AddSession(session); 96 | } 97 | 98 | public void RemoveSession(IXFireClient session) 99 | { 100 | _clientManager.RemoveSession(session); 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/Messages/Inbound/ClientPreferencesUpdate.cs: -------------------------------------------------------------------------------- 1 | using PFire.Core.Session; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | 5 | namespace PFire.Core.Protocol.Messages.Inbound 6 | { 7 | internal sealed class ClientPreferencesUpdate : XFireMessage 8 | { 9 | public ClientPreferencesUpdate() : base(XFireMessageType.ClientPreferencesUpdate) {} 10 | 11 | [XMessageField("prefs")] 12 | public Dictionary Prefs { get; set; } 13 | 14 | public override async Task Process(IXFireClient context) 15 | { 16 | //when "checked" in the client: Prefs does not contain the key 17 | //when "unchecked" in the client: Prefs contains the key the value will be "0" 18 | 19 | //Exception key 6: 20 | //when "checked" in the client: Prefs contains the key with the value "1" 21 | //when "unchecked" in the client: Prefs does not contain the key 22 | 23 | context.User.ClientPreferences.GameStatusShowMyFriends = !Prefs.ContainsKey((int)Enums.ClientPreferences.GameStatusShowMyFriends); 24 | context.User.ClientPreferences.GameStatusShowMyGameServer = !Prefs.ContainsKey((int)Enums.ClientPreferences.GameStatusShowMyGameServer); 25 | context.User.ClientPreferences.GameStatusShowMyProfile = !Prefs.ContainsKey((int)Enums.ClientPreferences.GameStatusShowMyProfile); 26 | context.User.ClientPreferences.PlaySoundSendOrReceiveMessage = !Prefs.ContainsKey((int)Enums.ClientPreferences.PlaySoundSendOrReceiveMessage); 27 | context.User.ClientPreferences.PlaySoundReceiveMessageWhileGaming = !Prefs.ContainsKey((int)Enums.ClientPreferences.PlaySoundReceiveMessageWhileGaming); 28 | context.User.ClientPreferences.ChatShowTimestamps = Prefs.ContainsKey((int)Enums.ClientPreferences.ChatShowTimestamps); 29 | context.User.ClientPreferences.PlaySoundFriendLogsOnOff = !Prefs.ContainsKey((int)Enums.ClientPreferences.PlaySoundFriendLogsOnOff); 30 | context.User.ClientPreferences.GameStatusShowFriendOfFriends = !Prefs.ContainsKey((int)Enums.ClientPreferences.GameStatusShowFriendOfFriends); 31 | context.User.ClientPreferences.ShowOfflineFriends = !Prefs.ContainsKey((int)Enums.ClientPreferences.ShowOfflineFriends); 32 | context.User.ClientPreferences.ShowNicknames = !Prefs.ContainsKey((int)Enums.ClientPreferences.ShowNicknames); 33 | context.User.ClientPreferences.ShowVoiceChatServerToFriends = !Prefs.ContainsKey((int)Enums.ClientPreferences.ShowVoiceChatServerToFriends); 34 | context.User.ClientPreferences.ShowWhenTyping = !Prefs.ContainsKey((int)Enums.ClientPreferences.ShowWhenTyping); 35 | context.User.ClientPreferences.NotificationFriendLogsOnOff = !Prefs.ContainsKey((int)Enums.ClientPreferences.NotificationFriendLogsOnOff); 36 | context.User.ClientPreferences.NotificationDownloadStartsFinishes = !Prefs.ContainsKey((int)Enums.ClientPreferences.NotificationDownloadStartsFinishes); 37 | context.User.ClientPreferences.PlaySoundSomeoneJoinsLeaveChatroom = !Prefs.ContainsKey((int)Enums.ClientPreferences.PlaySoundSomeoneJoinsLeaveChatroom); 38 | context.User.ClientPreferences.PlaySoundSendReceiveVoiceChatRequest = !Prefs.ContainsKey((int)Enums.ClientPreferences.PlaySoundSendReceiveVoiceChatRequest); 39 | context.User.ClientPreferences.PlaySoundScreenshotWhileGaming = !Prefs.ContainsKey((int)Enums.ClientPreferences.PlaySoundScreenshotWhileGaming); 40 | context.User.ClientPreferences.NotificationConnectionStateChanges = !Prefs.ContainsKey((int)Enums.ClientPreferences.NotificationConnectionStateChanges); 41 | 42 | await context.Server.Database.SaveClientPreferences(context.User); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /PFire.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30717.126 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PFire.Console", "src\PFire.Console\PFire.Console.csproj", "{D1ED9EB7-85DE-4D74-8D38-676F95A40442}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PFire.Core", "src\PFire.Core\PFire.Core.csproj", "{62FD083C-F667-44FA-99E7-7A35F363886B}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PFire.Infrastructure", "src\PFire.Infrastructure\PFire.Infrastructure.csproj", "{CB7EC8E0-436A-4E63-9F24-020238058DB2}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PFire.Tests", "tests\PFire.Tests\PFire.Tests.csproj", "{F28C1DF0-F867-4435-8165-7F1699994047}" 13 | EndProject 14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{C495825B-CE95-45B2-B737-0D3D78AC77F9}" 15 | EndProject 16 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{79216070-4B75-4D76-869A-38F852A6D407}" 17 | ProjectSection(SolutionItems) = preProject 18 | .editorconfig = .editorconfig 19 | LICENSE = LICENSE 20 | readme-screenshot.png = readme-screenshot.png 21 | README.md = README.md 22 | docs\XFireProtocol.md = docs\XFireProtocol.md 23 | EndProjectSection 24 | EndProject 25 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PFire.Common", "src\PFire.Common\PFire.Common.csproj", "{E2DCF6BC-9AAD-4361-B8C4-0F909A34096B}" 26 | EndProject 27 | Global 28 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 29 | Debug|Any CPU = Debug|Any CPU 30 | Release|Any CPU = Release|Any CPU 31 | EndGlobalSection 32 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 33 | {D1ED9EB7-85DE-4D74-8D38-676F95A40442}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {D1ED9EB7-85DE-4D74-8D38-676F95A40442}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {D1ED9EB7-85DE-4D74-8D38-676F95A40442}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {D1ED9EB7-85DE-4D74-8D38-676F95A40442}.Release|Any CPU.Build.0 = Release|Any CPU 37 | {62FD083C-F667-44FA-99E7-7A35F363886B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 38 | {62FD083C-F667-44FA-99E7-7A35F363886B}.Debug|Any CPU.Build.0 = Debug|Any CPU 39 | {62FD083C-F667-44FA-99E7-7A35F363886B}.Release|Any CPU.ActiveCfg = Release|Any CPU 40 | {62FD083C-F667-44FA-99E7-7A35F363886B}.Release|Any CPU.Build.0 = Release|Any CPU 41 | {CB7EC8E0-436A-4E63-9F24-020238058DB2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 42 | {CB7EC8E0-436A-4E63-9F24-020238058DB2}.Debug|Any CPU.Build.0 = Debug|Any CPU 43 | {CB7EC8E0-436A-4E63-9F24-020238058DB2}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {CB7EC8E0-436A-4E63-9F24-020238058DB2}.Release|Any CPU.Build.0 = Release|Any CPU 45 | {F28C1DF0-F867-4435-8165-7F1699994047}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 46 | {F28C1DF0-F867-4435-8165-7F1699994047}.Debug|Any CPU.Build.0 = Debug|Any CPU 47 | {F28C1DF0-F867-4435-8165-7F1699994047}.Release|Any CPU.ActiveCfg = Release|Any CPU 48 | {F28C1DF0-F867-4435-8165-7F1699994047}.Release|Any CPU.Build.0 = Release|Any CPU 49 | {E2DCF6BC-9AAD-4361-B8C4-0F909A34096B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 50 | {E2DCF6BC-9AAD-4361-B8C4-0F909A34096B}.Debug|Any CPU.Build.0 = Debug|Any CPU 51 | {E2DCF6BC-9AAD-4361-B8C4-0F909A34096B}.Release|Any CPU.ActiveCfg = Release|Any CPU 52 | {E2DCF6BC-9AAD-4361-B8C4-0F909A34096B}.Release|Any CPU.Build.0 = Release|Any CPU 53 | EndGlobalSection 54 | GlobalSection(SolutionProperties) = preSolution 55 | HideSolutionNode = FALSE 56 | EndGlobalSection 57 | GlobalSection(NestedProjects) = preSolution 58 | {F28C1DF0-F867-4435-8165-7F1699994047} = {C495825B-CE95-45B2-B737-0D3D78AC77F9} 59 | EndGlobalSection 60 | GlobalSection(ExtensibilityGlobals) = postSolution 61 | SolutionGuid = {62CCFE0A-411D-4A9C-8A9B-7AA029135BCA} 62 | EndGlobalSection 63 | EndGlobal 64 | -------------------------------------------------------------------------------- /src/PFire.Infrastructure/Migrations/20250307032454_ClientPreferences.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | #nullable disable 5 | 6 | namespace PFire.Infrastructure.Migrations 7 | { 8 | /// 9 | public partial class ClientPreferences : Migration 10 | { 11 | /// 12 | protected override void Up(MigrationBuilder migrationBuilder) 13 | { 14 | migrationBuilder.CreateTable( 15 | name: "ClientPreferences", 16 | columns: table => new 17 | { 18 | Id = table.Column(type: "INTEGER", nullable: false) 19 | .Annotation("Sqlite:Autoincrement", true), 20 | UserId = table.Column(type: "INTEGER", nullable: false), 21 | GameStatusShowMyFriends = table.Column(type: "INTEGER", nullable: false), 22 | GameStatusShowMyGameServer = table.Column(type: "INTEGER", nullable: false), 23 | GameStatusShowMyProfile = table.Column(type: "INTEGER", nullable: false), 24 | ChatShowTimestamps = table.Column(type: "INTEGER", nullable: false), 25 | ShowVoiceChatServerToFriends = table.Column(type: "INTEGER", nullable: false), 26 | ShowWhenTyping = table.Column(type: "INTEGER", nullable: false), 27 | GameStatusShowFriendOfFriends = table.Column(type: "INTEGER", nullable: false), 28 | PlaySoundSendOrReceiveMessage = table.Column(type: "INTEGER", nullable: false), 29 | PlaySoundReceiveMessageWhileGaming = table.Column(type: "INTEGER", nullable: false), 30 | PlaySoundFriendLogsOnOff = table.Column(type: "INTEGER", nullable: false), 31 | ShowOfflineFriends = table.Column(type: "INTEGER", nullable: false), 32 | ShowNicknames = table.Column(type: "INTEGER", nullable: false), 33 | NotificationFriendLogsOnOff = table.Column(type: "INTEGER", nullable: false), 34 | NotificationDownloadStartsFinishes = table.Column(type: "INTEGER", nullable: false), 35 | PlaySoundSomeoneJoinsLeaveChatroom = table.Column(type: "INTEGER", nullable: false), 36 | PlaySoundSendReceiveVoiceChatRequest = table.Column(type: "INTEGER", nullable: false), 37 | PlaySoundScreenshotWhileGaming = table.Column(type: "INTEGER", nullable: false), 38 | NotificationConnectionStateChanges = table.Column(type: "INTEGER", nullable: false), 39 | Version = table.Column(type: "BLOB", nullable: true), 40 | DateCreated = table.Column(type: "TEXT", nullable: false), 41 | DateModified = table.Column(type: "TEXT", nullable: true) 42 | }, 43 | constraints: table => 44 | { 45 | table.PrimaryKey("PK_ClientPreferences", x => x.Id); 46 | table.ForeignKey( 47 | name: "FK_ClientPreferences_User_UserId", 48 | column: x => x.UserId, 49 | principalTable: "User", 50 | principalColumn: "Id", 51 | onDelete: ReferentialAction.Cascade); 52 | }); 53 | 54 | migrationBuilder.CreateIndex( 55 | name: "IX_ClientPreferences_UserId", 56 | table: "ClientPreferences", 57 | column: "UserId", 58 | unique: true); 59 | } 60 | 61 | /// 62 | protected override void Down(MigrationBuilder migrationBuilder) 63 | { 64 | migrationBuilder.DropTable( 65 | name: "ClientPreferences"); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/PFire.Infrastructure/Migrations/20201213224005_InitialMigration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | namespace PFire.Infrastructure.Migrations 5 | { 6 | public partial class InitialMigration : Migration 7 | { 8 | protected override void Up(MigrationBuilder migrationBuilder) 9 | { 10 | migrationBuilder.CreateTable( 11 | name: "Users", 12 | columns: table => new 13 | { 14 | Id = table.Column(type: "INTEGER", nullable: false) 15 | .Annotation("Sqlite:Autoincrement", true), 16 | Username = table.Column(type: "TEXT", maxLength: 1000, nullable: false), 17 | Password = table.Column(type: "TEXT", nullable: false), 18 | Salt = table.Column(type: "TEXT", nullable: false), 19 | Nickname = table.Column(type: "TEXT", maxLength: 1000, nullable: true), 20 | Version = table.Column(type: "BLOB", rowVersion: true, nullable: true), 21 | DateCreated = table.Column(type: "TEXT", nullable: false), 22 | DateModified = table.Column(type: "TEXT", nullable: false) 23 | }, 24 | constraints: table => 25 | { 26 | table.PrimaryKey("PK_Users", x => x.Id); 27 | }); 28 | 29 | migrationBuilder.CreateTable( 30 | name: "Friends", 31 | columns: table => new 32 | { 33 | MeId = table.Column(type: "INTEGER", nullable: false), 34 | ThemId = table.Column(type: "INTEGER", nullable: false), 35 | Message = table.Column(type: "TEXT", maxLength: 1000, nullable: true), 36 | Pending = table.Column(type: "INTEGER", nullable: false), 37 | Version = table.Column(type: "BLOB", rowVersion: true, nullable: true), 38 | DateCreated = table.Column(type: "TEXT", nullable: false), 39 | DateModified = table.Column(type: "TEXT", nullable: false) 40 | }, 41 | constraints: table => 42 | { 43 | table.PrimaryKey("PK_Friends", x => new { x.MeId, x.ThemId }); 44 | table.ForeignKey( 45 | name: "FK_Friends_Users_MeId", 46 | column: x => x.MeId, 47 | principalTable: "Users", 48 | principalColumn: "Id", 49 | onDelete: ReferentialAction.Restrict); 50 | table.ForeignKey( 51 | name: "FK_Friends_Users_ThemId", 52 | column: x => x.ThemId, 53 | principalTable: "Users", 54 | principalColumn: "Id", 55 | onDelete: ReferentialAction.Restrict); 56 | }); 57 | 58 | migrationBuilder.CreateIndex( 59 | name: "IX_Friends_MeId_ThemId", 60 | table: "Friends", 61 | columns: new[] { "MeId", "ThemId" }, 62 | unique: true); 63 | 64 | migrationBuilder.CreateIndex( 65 | name: "IX_Friends_ThemId", 66 | table: "Friends", 67 | column: "ThemId"); 68 | 69 | migrationBuilder.CreateIndex( 70 | name: "IX_Users_Id", 71 | table: "Users", 72 | column: "Id"); 73 | 74 | migrationBuilder.CreateIndex( 75 | name: "IX_Users_Username", 76 | table: "Users", 77 | column: "Username"); 78 | } 79 | 80 | protected override void Down(MigrationBuilder migrationBuilder) 81 | { 82 | migrationBuilder.DropTable( 83 | name: "Friends"); 84 | 85 | migrationBuilder.DropTable( 86 | name: "Users"); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/Messages/Bidirectional/ChatMessage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.Extensions.Logging; 6 | using PFire.Core.Protocol.Messages.MessageEnums; 7 | using PFire.Core.Session; 8 | 9 | namespace PFire.Core.Protocol.Messages.Bidirectional 10 | { 11 | internal sealed class ChatMessage : XFireMessage 12 | { 13 | // the id of this message is the one that the original code base used 14 | // technically this is a server routed chat message 15 | public ChatMessage() : base(XFireMessageType.ServerChatMessage) {} 16 | 17 | [XMessageField("sid")] 18 | public Guid SessionId { get; set; } 19 | 20 | [XMessageField("peermsg")] 21 | public Dictionary MessagePayload { get; set; } 22 | 23 | // TODO: Create test for this message so we can refactor and build this message the same way as the others to avoid the switch statement 24 | // TODO: How to tell the client we didn't receive the ACK? 25 | // TODO: P2P stuff??? 26 | public override async Task Process(IXFireClient context) 27 | { 28 | var otherSession = context.Server.GetSession(SessionId); 29 | if (otherSession == null) 30 | { 31 | return; 32 | } 33 | 34 | var messageType = (ChatMessageType)(byte)MessagePayload["msgtype"]; 35 | 36 | switch (messageType) 37 | { 38 | case ChatMessageType.Content: 39 | var chatMsg = BuildChatMessageResponse(context.SessionId); 40 | await otherSession.SendMessage(chatMsg); 41 | 42 | break; 43 | 44 | case ChatMessageType.TypingNotification: 45 | var typingMsg = BuildChatMessageResponse(context.SessionId); 46 | await otherSession.SendMessage(typingMsg); 47 | break; 48 | 49 | case ChatMessageType.ClientInformation: 50 | var infoMsg = BuildClientInfo(context); 51 | await otherSession.SendMessage(infoMsg); 52 | break; 53 | 54 | case ChatMessageType.Acknowledgement: 55 | var ack = BuildChatMessageResponse(context.SessionId); 56 | await otherSession.SendMessage(ack); 57 | break; 58 | 59 | default: 60 | context.Logger.LogDebug($"NOT BUILT: Got {messageType} for session: {context.SessionId}"); 61 | break; 62 | } 63 | } 64 | 65 | private ChatMessage BuildChatMessageResponse(Guid sessionId) 66 | { 67 | return new ChatMessage 68 | { 69 | SessionId = sessionId, 70 | MessagePayload = new Dictionary(MessagePayload) 71 | }; 72 | } 73 | 74 | private ChatMessage BuildAckResponse(Guid sessionId) 75 | { 76 | return new ChatMessage 77 | { 78 | SessionId = sessionId, 79 | MessagePayload = new Dictionary 80 | { 81 | {"msgtyp", (byte)ChatMessageType.Acknowledgement}, 82 | {"imindex", (int)MessagePayload["imindex"]} 83 | } 84 | }; 85 | } 86 | 87 | private ChatMessage BuildClientInfo(IXFireClient client) 88 | { 89 | var info = new ChatMessage 90 | { 91 | SessionId = client.SessionId, 92 | MessagePayload = new Dictionary 93 | { 94 | {"msgtyp", (byte)ChatMessageType.ClientInformation} 95 | } 96 | }; 97 | 98 | info.MessagePayload.Add("ip", client.RemoteEndPoint.ToString()?.Split(":").First()); 99 | info.MessagePayload.Add("port", 50_000); 100 | info.MessagePayload.Add("localip", "192.168.1.38"); 101 | info.MessagePayload.Add("localport", 50_000); 102 | info.MessagePayload.Add("status", 0); 103 | info.MessagePayload.Add("salt", client.Salt); 104 | return info; 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/PFire.Infrastructure/Migrations/20201213224005_InitialMigration.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Migrations; 6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 7 | using PFire.Infrastructure.Services; 8 | 9 | namespace PFire.Infrastructure.Migrations 10 | { 11 | [DbContext(typeof(DatabaseContext))] 12 | [Migration("20201213224005_InitialMigration")] 13 | partial class InitialMigration 14 | { 15 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 16 | { 17 | #pragma warning disable 612, 618 18 | modelBuilder 19 | .HasAnnotation("ProductVersion", "5.0.0"); 20 | 21 | modelBuilder.Entity("PFire.Data.Entities.Friend", b => 22 | { 23 | b.Property("MeId") 24 | .HasColumnType("INTEGER"); 25 | 26 | b.Property("ThemId") 27 | .HasColumnType("INTEGER"); 28 | 29 | b.Property("DateCreated") 30 | .HasColumnType("TEXT"); 31 | 32 | b.Property("DateModified") 33 | .HasColumnType("TEXT"); 34 | 35 | b.Property("Message") 36 | .HasMaxLength(1000) 37 | .HasColumnType("TEXT"); 38 | 39 | b.Property("Pending") 40 | .HasColumnType("INTEGER"); 41 | 42 | b.Property("Version") 43 | .IsConcurrencyToken() 44 | .ValueGeneratedOnAddOrUpdate() 45 | .HasColumnType("BLOB"); 46 | 47 | b.HasKey("MeId", "ThemId"); 48 | 49 | b.HasIndex("ThemId"); 50 | 51 | b.HasIndex("MeId", "ThemId") 52 | .IsUnique(); 53 | 54 | b.ToTable("Friends"); 55 | }); 56 | 57 | modelBuilder.Entity("PFire.Data.Entities.User", b => 58 | { 59 | b.Property("Id") 60 | .ValueGeneratedOnAdd() 61 | .HasColumnType("INTEGER"); 62 | 63 | b.Property("DateCreated") 64 | .HasColumnType("TEXT"); 65 | 66 | b.Property("DateModified") 67 | .HasColumnType("TEXT"); 68 | 69 | b.Property("Nickname") 70 | .HasMaxLength(1000) 71 | .HasColumnType("TEXT"); 72 | 73 | b.Property("Password") 74 | .IsRequired() 75 | .HasColumnType("TEXT"); 76 | 77 | b.Property("Salt") 78 | .IsRequired() 79 | .HasColumnType("TEXT"); 80 | 81 | b.Property("Username") 82 | .IsRequired() 83 | .HasMaxLength(1000) 84 | .HasColumnType("TEXT"); 85 | 86 | b.Property("Version") 87 | .IsConcurrencyToken() 88 | .ValueGeneratedOnAddOrUpdate() 89 | .HasColumnType("BLOB"); 90 | 91 | b.HasKey("Id"); 92 | 93 | b.HasIndex("Id"); 94 | 95 | b.HasIndex("Username"); 96 | 97 | b.ToTable("Users"); 98 | }); 99 | 100 | modelBuilder.Entity("PFire.Data.Entities.Friend", b => 101 | { 102 | b.HasOne("PFire.Data.Entities.User", "Me") 103 | .WithMany("MyFriends") 104 | .HasForeignKey("MeId") 105 | .OnDelete(DeleteBehavior.Restrict) 106 | .IsRequired(); 107 | 108 | b.HasOne("PFire.Data.Entities.User", "Them") 109 | .WithMany("FriendsOf") 110 | .HasForeignKey("ThemId") 111 | .OnDelete(DeleteBehavior.Restrict) 112 | .IsRequired(); 113 | 114 | b.Navigation("Me"); 115 | 116 | b.Navigation("Them"); 117 | }); 118 | 119 | modelBuilder.Entity("PFire.Data.Entities.User", b => 120 | { 121 | b.Navigation("FriendsOf"); 122 | 123 | b.Navigation("MyFriends"); 124 | }); 125 | #pragma warning restore 612, 618 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/PFire.Infrastructure/Migrations/20201219103931_FixTableNames.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Migrations; 6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 7 | using PFire.Infrastructure.Services; 8 | 9 | namespace PFire.Infrastructure.Migrations 10 | { 11 | [DbContext(typeof(DatabaseContext))] 12 | [Migration("20201219103931_FixTableNames")] 13 | partial class FixTableNames 14 | { 15 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 16 | { 17 | #pragma warning disable 612, 618 18 | modelBuilder 19 | .HasAnnotation("ProductVersion", "5.0.0"); 20 | 21 | modelBuilder.Entity("PFire.Infrastructure.Entities.Friend", b => 22 | { 23 | b.Property("MeId") 24 | .HasColumnType("INTEGER"); 25 | 26 | b.Property("ThemId") 27 | .HasColumnType("INTEGER"); 28 | 29 | b.Property("DateCreated") 30 | .HasColumnType("TEXT"); 31 | 32 | b.Property("DateModified") 33 | .HasColumnType("TEXT"); 34 | 35 | b.Property("Message") 36 | .HasMaxLength(1000) 37 | .HasColumnType("TEXT"); 38 | 39 | b.Property("Pending") 40 | .HasColumnType("INTEGER"); 41 | 42 | b.Property("Version") 43 | .IsConcurrencyToken() 44 | .ValueGeneratedOnAddOrUpdate() 45 | .HasColumnType("BLOB"); 46 | 47 | b.HasKey("MeId", "ThemId"); 48 | 49 | b.HasIndex("ThemId"); 50 | 51 | b.HasIndex("MeId", "ThemId") 52 | .IsUnique(); 53 | 54 | b.ToTable("Friend"); 55 | }); 56 | 57 | modelBuilder.Entity("PFire.Infrastructure.Entities.User", b => 58 | { 59 | b.Property("Id") 60 | .ValueGeneratedOnAdd() 61 | .HasColumnType("INTEGER"); 62 | 63 | b.Property("DateCreated") 64 | .HasColumnType("TEXT"); 65 | 66 | b.Property("DateModified") 67 | .HasColumnType("TEXT"); 68 | 69 | b.Property("Nickname") 70 | .HasMaxLength(1000) 71 | .HasColumnType("TEXT"); 72 | 73 | b.Property("Password") 74 | .IsRequired() 75 | .HasColumnType("TEXT"); 76 | 77 | b.Property("Salt") 78 | .IsRequired() 79 | .HasColumnType("TEXT"); 80 | 81 | b.Property("Username") 82 | .IsRequired() 83 | .HasMaxLength(1000) 84 | .HasColumnType("TEXT"); 85 | 86 | b.Property("Version") 87 | .IsConcurrencyToken() 88 | .ValueGeneratedOnAddOrUpdate() 89 | .HasColumnType("BLOB"); 90 | 91 | b.HasKey("Id"); 92 | 93 | b.HasIndex("Id"); 94 | 95 | b.HasIndex("Username"); 96 | 97 | b.ToTable("User"); 98 | }); 99 | 100 | modelBuilder.Entity("PFire.Infrastructure.Entities.Friend", b => 101 | { 102 | b.HasOne("PFire.Infrastructure.Entities.User", "Me") 103 | .WithMany("MyFriends") 104 | .HasForeignKey("MeId") 105 | .OnDelete(DeleteBehavior.Restrict) 106 | .IsRequired(); 107 | 108 | b.HasOne("PFire.Infrastructure.Entities.User", "Them") 109 | .WithMany("FriendsOf") 110 | .HasForeignKey("ThemId") 111 | .OnDelete(DeleteBehavior.Restrict) 112 | .IsRequired(); 113 | 114 | b.Navigation("Me"); 115 | 116 | b.Navigation("Them"); 117 | }); 118 | 119 | modelBuilder.Entity("PFire.Infrastructure.Entities.User", b => 120 | { 121 | b.Navigation("FriendsOf"); 122 | 123 | b.Navigation("MyFriends"); 124 | }); 125 | #pragma warning restore 612, 618 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/PFire.Core/Protocol/MessageSerializer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Reflection; 7 | using System.Text; 8 | using PFire.Core.Protocol.Messages; 9 | using PFire.Core.Protocol.Messages.Inbound; 10 | using PFire.Core.Util; 11 | 12 | namespace PFire.Core.Protocol 13 | { 14 | internal static class MessageSerializer 15 | { 16 | private static readonly int MESSAGE_SIZE_LENGTH_IN_BYTES = 2; 17 | 18 | public static IMessage Deserialize(byte[] data) 19 | { 20 | using var reader = new BinaryReader(new MemoryStream(data)); 21 | var messageTypeId = reader.ReadInt16(); 22 | var xMessageType = (XFireMessageType)messageTypeId; 23 | 24 | var messageType = XFireMessageTypeFactory.Instance.GetMessageType(xMessageType); 25 | var message = Activator.CreateInstance(messageType) as IMessage; 26 | return Deserialize(reader, message); 27 | } 28 | 29 | public static IMessage Deserialize(BinaryReader reader, IMessage messageBase) 30 | { 31 | var messageType = messageBase.GetType(); 32 | var fieldInfo = messageType.GetProperties(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public); 33 | 34 | var attributeCount = reader.ReadByte(); 35 | 36 | for (var i = 0; i < attributeCount; i++) 37 | { 38 | var attributeName = GetAttributeName(reader, messageType); 39 | 40 | var attributeType = reader.ReadByte(); 41 | 42 | var value = XFireAttributeFactory.Instance.GetAttribute(attributeType).ReadValue(reader); 43 | 44 | var field = fieldInfo.Where(a => a.GetCustomAttribute() != null) 45 | .FirstOrDefault(a => a.GetCustomAttribute()?.Name == attributeName); 46 | 47 | if (field != null) 48 | { 49 | field.SetValue(messageBase, value); 50 | } 51 | else 52 | { 53 | Debug.WriteLine($"WARN: No attribute defined for {attributeName} on class {messageType.Name}"); 54 | } 55 | } 56 | 57 | Debug.WriteLine($"Deserialized [{messageType}]: {messageBase}"); 58 | 59 | return messageBase; 60 | } 61 | 62 | private static string GetAttributeName(BinaryReader reader, Type messageType) 63 | { 64 | // TODO: Be brave enough to find an elegant fix for this 65 | // XFire decides not to follow its own rules. Message type 32 does not have a prefix byte for the length of the attribute name 66 | // and breaks this code. Assume first byte after the attribute count as the attribute name 67 | List byteTypes = 68 | [ 69 | typeof(StatusChange), 70 | typeof(GameServerFetchAll), 71 | typeof(GroupCreate), 72 | typeof(GroupRemove), 73 | typeof(GroupRename) 74 | ]; 75 | 76 | var count = byteTypes.Contains(messageType) ? 1 : reader.ReadByte(); 77 | var readBytes = reader.ReadBytes(count); 78 | 79 | return Encoding.UTF8.GetString(readBytes); 80 | } 81 | 82 | public static byte[] Serialize(IMessage message) 83 | { 84 | var payload = WritePayloadFromMessage(message); 85 | var payloadShort = (short)(payload.Length + MESSAGE_SIZE_LENGTH_IN_BYTES); 86 | var payloadLength = BitConverter.GetBytes(payloadShort); 87 | 88 | var finalPayload = ByteHelper.CombineByteArray(payloadLength, payload); 89 | 90 | Debug.WriteLine($"Serialized [{message}]: {BitConverter.ToString(finalPayload)}"); 91 | 92 | return finalPayload; 93 | } 94 | 95 | private static byte[] WritePayloadFromMessage(IMessage message) 96 | { 97 | var propertyInfo = message.GetType().GetProperties(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public); 98 | var attributesToBeWritten = new List>(); 99 | propertyInfo.Where(a => Attribute.IsDefined(a, typeof(XMessageField))) 100 | .ToList() 101 | .ForEach(property => 102 | { 103 | var propertyValue = property.GetValue(message); 104 | var attributeDefinition = property.GetCustomAttribute(); 105 | var attribute = XFireAttributeFactory.Instance.GetAttribute(property.PropertyType); 106 | 107 | attributesToBeWritten.Add( 108 | Tuple.Create( 109 | attributeDefinition, 110 | attribute.AttributeTypeId, 111 | propertyValue 112 | ) 113 | ); 114 | }); 115 | 116 | using var ms = new MemoryStream(); 117 | using var writer = new BinaryWriter(ms); 118 | writer.Write((short)message.MessageTypeId); 119 | writer.Write((byte)attributesToBeWritten.Count); 120 | attributesToBeWritten.ForEach(a => 121 | { 122 | var attribute = XFireAttributeFactory.Instance.GetAttribute(a.Item2); 123 | if (a.Item1.NonTextualName) 124 | { 125 | attribute.WriteNameWithoutLengthPrefix(writer, a.Item1.NameAsBytes); 126 | attribute.WriteType(writer); 127 | attribute.WriteValue(writer, a.Item3); 128 | } 129 | else 130 | { 131 | attribute.WriteAll(writer, a.Item1.Name, a.Item3); 132 | } 133 | }); 134 | 135 | return ms.ToArray(); 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/PFire.Infrastructure/Migrations/20250120173254_CreateGroup.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Migrations; 6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 7 | using PFire.Infrastructure.Services; 8 | 9 | #nullable disable 10 | 11 | namespace PFire.Infrastructure.Migrations 12 | { 13 | [DbContext(typeof(DatabaseContext))] 14 | [Migration("20250120173254_CreateGroup")] 15 | partial class CreateGroup 16 | { 17 | /// 18 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 19 | { 20 | #pragma warning disable 612, 618 21 | modelBuilder.HasAnnotation("ProductVersion", "9.0.1"); 22 | 23 | modelBuilder.Entity("PFire.Infrastructure.Entities.Friend", b => 24 | { 25 | b.Property("MeId") 26 | .HasColumnType("INTEGER"); 27 | 28 | b.Property("ThemId") 29 | .HasColumnType("INTEGER"); 30 | 31 | b.Property("DateCreated") 32 | .HasColumnType("TEXT"); 33 | 34 | b.Property("DateModified") 35 | .HasColumnType("TEXT"); 36 | 37 | b.Property("Message") 38 | .HasMaxLength(1000) 39 | .HasColumnType("TEXT"); 40 | 41 | b.Property("Pending") 42 | .HasColumnType("INTEGER"); 43 | 44 | b.Property("Version") 45 | .IsConcurrencyToken() 46 | .ValueGeneratedOnAddOrUpdate() 47 | .HasColumnType("BLOB"); 48 | 49 | b.HasKey("MeId", "ThemId"); 50 | 51 | b.HasIndex("ThemId"); 52 | 53 | b.HasIndex("MeId", "ThemId") 54 | .IsUnique(); 55 | 56 | b.ToTable("Friend", (string)null); 57 | }); 58 | 59 | modelBuilder.Entity("PFire.Infrastructure.Entities.Group", b => 60 | { 61 | b.Property("Id") 62 | .ValueGeneratedOnAdd() 63 | .HasColumnType("INTEGER"); 64 | 65 | b.Property("DateCreated") 66 | .HasColumnType("TEXT"); 67 | 68 | b.Property("DateModified") 69 | .HasColumnType("TEXT"); 70 | 71 | b.Property("Name") 72 | .IsRequired() 73 | .HasMaxLength(1000) 74 | .HasColumnType("TEXT"); 75 | 76 | b.Property("OwnerId") 77 | .HasColumnType("INTEGER"); 78 | 79 | b.Property("Version") 80 | .IsConcurrencyToken() 81 | .ValueGeneratedOnAddOrUpdate() 82 | .HasColumnType("BLOB"); 83 | 84 | b.HasKey("Id"); 85 | 86 | b.HasIndex("OwnerId"); 87 | 88 | b.ToTable("Group"); 89 | }); 90 | 91 | modelBuilder.Entity("PFire.Infrastructure.Entities.User", b => 92 | { 93 | b.Property("Id") 94 | .ValueGeneratedOnAdd() 95 | .HasColumnType("INTEGER"); 96 | 97 | b.Property("DateCreated") 98 | .HasColumnType("TEXT"); 99 | 100 | b.Property("DateModified") 101 | .HasColumnType("TEXT"); 102 | 103 | b.Property("Nickname") 104 | .HasMaxLength(1000) 105 | .HasColumnType("TEXT"); 106 | 107 | b.Property("Password") 108 | .IsRequired() 109 | .HasColumnType("TEXT"); 110 | 111 | b.Property("Salt") 112 | .IsRequired() 113 | .HasColumnType("TEXT"); 114 | 115 | b.Property("Username") 116 | .IsRequired() 117 | .HasMaxLength(1000) 118 | .HasColumnType("TEXT"); 119 | 120 | b.Property("Version") 121 | .IsConcurrencyToken() 122 | .ValueGeneratedOnAddOrUpdate() 123 | .HasColumnType("BLOB"); 124 | 125 | b.HasKey("Id"); 126 | 127 | b.HasIndex("Id"); 128 | 129 | b.HasIndex("Username"); 130 | 131 | b.ToTable("User", (string)null); 132 | }); 133 | 134 | modelBuilder.Entity("PFire.Infrastructure.Entities.Friend", b => 135 | { 136 | b.HasOne("PFire.Infrastructure.Entities.User", "Me") 137 | .WithMany("MyFriends") 138 | .HasForeignKey("MeId") 139 | .OnDelete(DeleteBehavior.Restrict) 140 | .IsRequired(); 141 | 142 | b.HasOne("PFire.Infrastructure.Entities.User", "Them") 143 | .WithMany("FriendsOf") 144 | .HasForeignKey("ThemId") 145 | .OnDelete(DeleteBehavior.Restrict) 146 | .IsRequired(); 147 | 148 | b.Navigation("Me"); 149 | 150 | b.Navigation("Them"); 151 | }); 152 | 153 | modelBuilder.Entity("PFire.Infrastructure.Entities.Group", b => 154 | { 155 | b.HasOne("PFire.Infrastructure.Entities.User", "Owner") 156 | .WithMany() 157 | .HasForeignKey("OwnerId") 158 | .OnDelete(DeleteBehavior.Cascade) 159 | .IsRequired(); 160 | 161 | b.Navigation("Owner"); 162 | }); 163 | 164 | modelBuilder.Entity("PFire.Infrastructure.Entities.User", b => 165 | { 166 | b.Navigation("FriendsOf"); 167 | 168 | b.Navigation("MyFriends"); 169 | }); 170 | #pragma warning restore 612, 618 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/PFire.Infrastructure/Migrations/20250202021012_ModifyAuditEntities.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Migrations; 6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 7 | using PFire.Infrastructure.Services; 8 | 9 | #nullable disable 10 | 11 | namespace PFire.Infrastructure.Migrations 12 | { 13 | [DbContext(typeof(DatabaseContext))] 14 | [Migration("20250202021012_ModifyAuditEntities")] 15 | partial class ModifyAuditEntities 16 | { 17 | /// 18 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 19 | { 20 | #pragma warning disable 612, 618 21 | modelBuilder.HasAnnotation("ProductVersion", "9.0.1"); 22 | 23 | modelBuilder.Entity("PFire.Infrastructure.Entities.Friend", b => 24 | { 25 | b.Property("MeId") 26 | .HasColumnType("INTEGER"); 27 | 28 | b.Property("ThemId") 29 | .HasColumnType("INTEGER"); 30 | 31 | b.Property("DateCreated") 32 | .HasColumnType("TEXT"); 33 | 34 | b.Property("DateModified") 35 | .HasColumnType("TEXT"); 36 | 37 | b.Property("Message") 38 | .HasMaxLength(1000) 39 | .HasColumnType("TEXT"); 40 | 41 | b.Property("Pending") 42 | .HasColumnType("INTEGER"); 43 | 44 | b.Property("Version") 45 | .IsConcurrencyToken() 46 | .ValueGeneratedOnAddOrUpdate() 47 | .HasColumnType("BLOB"); 48 | 49 | b.HasKey("MeId", "ThemId"); 50 | 51 | b.HasIndex("ThemId"); 52 | 53 | b.HasIndex("MeId", "ThemId") 54 | .IsUnique(); 55 | 56 | b.ToTable("Friend", (string)null); 57 | }); 58 | 59 | modelBuilder.Entity("PFire.Infrastructure.Entities.Group", b => 60 | { 61 | b.Property("Id") 62 | .ValueGeneratedOnAdd() 63 | .HasColumnType("INTEGER"); 64 | 65 | b.Property("DateCreated") 66 | .HasColumnType("TEXT"); 67 | 68 | b.Property("DateModified") 69 | .HasColumnType("TEXT"); 70 | 71 | b.Property("Name") 72 | .IsRequired() 73 | .HasMaxLength(1000) 74 | .HasColumnType("TEXT"); 75 | 76 | b.Property("OwnerId") 77 | .HasColumnType("INTEGER"); 78 | 79 | b.Property("Version") 80 | .IsConcurrencyToken() 81 | .ValueGeneratedOnAddOrUpdate() 82 | .HasColumnType("BLOB"); 83 | 84 | b.HasKey("Id"); 85 | 86 | b.HasIndex("OwnerId"); 87 | 88 | b.ToTable("Group"); 89 | }); 90 | 91 | modelBuilder.Entity("PFire.Infrastructure.Entities.User", b => 92 | { 93 | b.Property("Id") 94 | .ValueGeneratedOnAdd() 95 | .HasColumnType("INTEGER"); 96 | 97 | b.Property("DateCreated") 98 | .HasColumnType("TEXT"); 99 | 100 | b.Property("DateModified") 101 | .HasColumnType("TEXT"); 102 | 103 | b.Property("Nickname") 104 | .HasMaxLength(1000) 105 | .HasColumnType("TEXT"); 106 | 107 | b.Property("Password") 108 | .IsRequired() 109 | .HasColumnType("TEXT"); 110 | 111 | b.Property("Salt") 112 | .IsRequired() 113 | .HasColumnType("TEXT"); 114 | 115 | b.Property("Username") 116 | .IsRequired() 117 | .HasMaxLength(1000) 118 | .HasColumnType("TEXT"); 119 | 120 | b.Property("Version") 121 | .IsConcurrencyToken() 122 | .ValueGeneratedOnAddOrUpdate() 123 | .HasColumnType("BLOB"); 124 | 125 | b.HasKey("Id"); 126 | 127 | b.HasIndex("Id"); 128 | 129 | b.HasIndex("Username"); 130 | 131 | b.ToTable("User", (string)null); 132 | }); 133 | 134 | modelBuilder.Entity("PFire.Infrastructure.Entities.Friend", b => 135 | { 136 | b.HasOne("PFire.Infrastructure.Entities.User", "Me") 137 | .WithMany("MyFriends") 138 | .HasForeignKey("MeId") 139 | .OnDelete(DeleteBehavior.Restrict) 140 | .IsRequired(); 141 | 142 | b.HasOne("PFire.Infrastructure.Entities.User", "Them") 143 | .WithMany("FriendsOf") 144 | .HasForeignKey("ThemId") 145 | .OnDelete(DeleteBehavior.Restrict) 146 | .IsRequired(); 147 | 148 | b.Navigation("Me"); 149 | 150 | b.Navigation("Them"); 151 | }); 152 | 153 | modelBuilder.Entity("PFire.Infrastructure.Entities.Group", b => 154 | { 155 | b.HasOne("PFire.Infrastructure.Entities.User", "Owner") 156 | .WithMany() 157 | .HasForeignKey("OwnerId") 158 | .OnDelete(DeleteBehavior.Cascade) 159 | .IsRequired(); 160 | 161 | b.Navigation("Owner"); 162 | }); 163 | 164 | modelBuilder.Entity("PFire.Infrastructure.Entities.User", b => 165 | { 166 | b.Navigation("FriendsOf"); 167 | 168 | b.Navigation("MyFriends"); 169 | }); 170 | #pragma warning restore 612, 618 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | # C# files 4 | [*.cs] 5 | 6 | #### Core EditorConfig Options #### 7 | 8 | # Indentation and spacing 9 | indent_size = 4 10 | indent_style = space 11 | tab_width = 4 12 | 13 | # New line preferences 14 | end_of_line = crlf 15 | insert_final_newline = true 16 | 17 | #### .NET Coding Conventions #### 18 | 19 | # Organize usings 20 | dotnet_separate_import_directive_groups = false 21 | dotnet_sort_system_directives_first = true 22 | file_header_template = unset 23 | 24 | # this. and Me. preferences 25 | dotnet_style_qualification_for_event = false:silent 26 | dotnet_style_qualification_for_field = false:silent 27 | dotnet_style_qualification_for_method = false:silent 28 | dotnet_style_qualification_for_property = false:silent 29 | 30 | # Language keywords vs BCL types preferences 31 | dotnet_style_predefined_type_for_locals_parameters_members = true:silent 32 | dotnet_style_predefined_type_for_member_access = true:silent 33 | 34 | # Parentheses preferences 35 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent 36 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent 37 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent 38 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent 39 | 40 | # Modifier preferences 41 | dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent 42 | 43 | # Expression-level preferences 44 | dotnet_style_coalesce_expression = true:suggestion 45 | dotnet_style_collection_initializer = true:suggestion 46 | dotnet_style_explicit_tuple_names = true:suggestion 47 | dotnet_style_null_propagation = true:suggestion 48 | dotnet_style_object_initializer = true:suggestion 49 | dotnet_style_operator_placement_when_wrapping = beginning_of_line 50 | dotnet_style_prefer_auto_properties = true:silent 51 | dotnet_style_prefer_compound_assignment = true:suggestion 52 | dotnet_style_prefer_conditional_expression_over_assignment = true:silent 53 | dotnet_style_prefer_conditional_expression_over_return = true:silent 54 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion 55 | dotnet_style_prefer_inferred_tuple_names = true:suggestion 56 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion 57 | dotnet_style_prefer_simplified_boolean_expressions = true:suggestion 58 | dotnet_style_prefer_simplified_interpolation = true:suggestion 59 | 60 | # Field preferences 61 | dotnet_style_readonly_field = true:suggestion 62 | 63 | # Parameter preferences 64 | dotnet_code_quality_unused_parameters = all:suggestion 65 | 66 | # Suppression preferences 67 | dotnet_remove_unnecessary_suppression_exclusions = none 68 | 69 | #### C# Coding Conventions #### 70 | 71 | # var preferences 72 | csharp_style_var_elsewhere = true:silent 73 | csharp_style_var_for_built_in_types = true:silent 74 | csharp_style_var_when_type_is_apparent = true:silent 75 | 76 | # Expression-bodied members 77 | csharp_style_expression_bodied_accessors = true:silent 78 | csharp_style_expression_bodied_constructors = false:silent 79 | csharp_style_expression_bodied_indexers = true:silent 80 | csharp_style_expression_bodied_lambdas = true:silent 81 | csharp_style_expression_bodied_local_functions = false:silent 82 | csharp_style_expression_bodied_methods = false:silent 83 | csharp_style_expression_bodied_operators = false:silent 84 | csharp_style_expression_bodied_properties = true:silent 85 | 86 | # Pattern matching preferences 87 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 88 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 89 | csharp_style_prefer_not_pattern = true:suggestion 90 | csharp_style_prefer_pattern_matching = true:silent 91 | csharp_style_prefer_switch_expression = true:suggestion 92 | 93 | # Null-checking preferences 94 | csharp_style_conditional_delegate_call = true:suggestion 95 | 96 | # Modifier preferences 97 | csharp_prefer_static_local_function = true:suggestion 98 | csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:silent 99 | 100 | # Code-block preferences 101 | csharp_prefer_braces = true:silent 102 | csharp_prefer_simple_using_statement = true:suggestion 103 | 104 | # Expression-level preferences 105 | csharp_prefer_simple_default_expression = true:suggestion 106 | csharp_style_deconstructed_variable_declaration = true:suggestion 107 | csharp_style_inlined_variable_declaration = true:suggestion 108 | csharp_style_pattern_local_over_anonymous_function = true:suggestion 109 | csharp_style_prefer_index_operator = true:suggestion 110 | csharp_style_prefer_range_operator = true:suggestion 111 | csharp_style_throw_expression = true:suggestion 112 | csharp_style_unused_value_assignment_preference = discard_variable:suggestion 113 | csharp_style_unused_value_expression_statement_preference = discard_variable:silent 114 | 115 | # 'using' directive preferences 116 | csharp_using_directive_placement = outside_namespace:silent 117 | 118 | #### C# Formatting Rules #### 119 | 120 | # New line preferences 121 | csharp_new_line_before_catch = true 122 | csharp_new_line_before_else = true 123 | csharp_new_line_before_finally = true 124 | csharp_new_line_before_members_in_anonymous_types = true 125 | csharp_new_line_before_members_in_object_initializers = true 126 | csharp_new_line_before_open_brace = all 127 | csharp_new_line_between_query_expression_clauses = true 128 | 129 | # Indentation preferences 130 | csharp_indent_block_contents = true 131 | csharp_indent_braces = false 132 | csharp_indent_case_contents = true 133 | csharp_indent_case_contents_when_block = false 134 | csharp_indent_labels = no_change 135 | csharp_indent_switch_labels = true 136 | 137 | # Space preferences 138 | csharp_space_after_cast = false 139 | csharp_space_after_colon_in_inheritance_clause = true 140 | csharp_space_after_comma = true 141 | csharp_space_after_dot = false 142 | csharp_space_after_keywords_in_control_flow_statements = true 143 | csharp_space_after_semicolon_in_for_statement = true 144 | csharp_space_around_binary_operators = before_and_after 145 | csharp_space_around_declaration_statements = false 146 | csharp_space_before_colon_in_inheritance_clause = true 147 | csharp_space_before_comma = false 148 | csharp_space_before_dot = false 149 | csharp_space_before_open_square_brackets = false 150 | csharp_space_before_semicolon_in_for_statement = false 151 | csharp_space_between_empty_square_brackets = false 152 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 153 | csharp_space_between_method_call_name_and_opening_parenthesis = false 154 | csharp_space_between_method_call_parameter_list_parentheses = false 155 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 156 | csharp_space_between_method_declaration_name_and_open_parenthesis = false 157 | csharp_space_between_method_declaration_parameter_list_parentheses = false 158 | csharp_space_between_parentheses = false 159 | csharp_space_between_square_brackets = false 160 | 161 | # Wrapping preferences 162 | csharp_preserve_single_line_blocks = true 163 | csharp_preserve_single_line_statements = true 164 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | [Aa][Rr][Mm]/ 24 | [Aa][Rr][Mm]64/ 25 | bld/ 26 | [Bb]in/ 27 | [Oo]bj/ 28 | [Ll]og/ 29 | 30 | # Visual Studio 2015/2017 cache/options directory 31 | .vs/ 32 | # Uncomment if you have tasks that create the project's static files in wwwroot 33 | #wwwroot/ 34 | 35 | # Visual Studio 2017 auto generated files 36 | Generated\ Files/ 37 | 38 | # MSTest test Results 39 | [Tt]est[Rr]esult*/ 40 | [Bb]uild[Ll]og.* 41 | 42 | # NUNIT 43 | *.VisualState.xml 44 | TestResult.xml 45 | 46 | # Build Results of an ATL Project 47 | [Dd]ebugPS/ 48 | [Rr]eleasePS/ 49 | dlldata.c 50 | 51 | # Benchmark Results 52 | BenchmarkDotNet.Artifacts/ 53 | 54 | # .NET Core 55 | project.lock.json 56 | project.fragment.lock.json 57 | artifacts/ 58 | 59 | # StyleCop 60 | StyleCopReport.xml 61 | 62 | # Files built by Visual Studio 63 | *_i.c 64 | *_p.c 65 | *_h.h 66 | *.ilk 67 | *.meta 68 | *.obj 69 | *.iobj 70 | *.pch 71 | *.pdb 72 | *.ipdb 73 | *.pgc 74 | *.pgd 75 | *.rsp 76 | *.sbr 77 | *.tlb 78 | *.tli 79 | *.tlh 80 | *.tmp 81 | *.tmp_proj 82 | *_wpftmp.csproj 83 | *.log 84 | *.vspscc 85 | *.vssscc 86 | .builds 87 | *.pidb 88 | *.svclog 89 | *.scc 90 | 91 | # Chutzpah Test files 92 | _Chutzpah* 93 | 94 | # Visual C++ cache files 95 | ipch/ 96 | *.aps 97 | *.ncb 98 | *.opendb 99 | *.opensdf 100 | *.sdf 101 | *.cachefile 102 | *.VC.db 103 | *.VC.VC.opendb 104 | 105 | # Visual Studio profiler 106 | *.psess 107 | *.vsp 108 | *.vspx 109 | *.sap 110 | 111 | # Visual Studio Trace Files 112 | *.e2e 113 | 114 | # TFS 2012 Local Workspace 115 | $tf/ 116 | 117 | # Guidance Automation Toolkit 118 | *.gpState 119 | 120 | # ReSharper is a .NET coding add-in 121 | _ReSharper*/ 122 | *.[Rr]e[Ss]harper 123 | *.DotSettings.user 124 | 125 | # JustCode is a .NET coding add-in 126 | .JustCode 127 | 128 | # TeamCity is a build add-in 129 | _TeamCity* 130 | 131 | # DotCover is a Code Coverage Tool 132 | *.dotCover 133 | 134 | # AxoCover is a Code Coverage Tool 135 | .axoCover/* 136 | !.axoCover/settings.json 137 | 138 | # Visual Studio code coverage results 139 | *.coverage 140 | *.coveragexml 141 | 142 | # NCrunch 143 | _NCrunch_* 144 | .*crunch*.local.xml 145 | nCrunchTemp_* 146 | 147 | # MightyMoose 148 | *.mm.* 149 | AutoTest.Net/ 150 | 151 | # Web workbench (sass) 152 | .sass-cache/ 153 | 154 | # Installshield output folder 155 | [Ee]xpress/ 156 | 157 | # DocProject is a documentation generator add-in 158 | DocProject/buildhelp/ 159 | DocProject/Help/*.HxT 160 | DocProject/Help/*.HxC 161 | DocProject/Help/*.hhc 162 | DocProject/Help/*.hhk 163 | DocProject/Help/*.hhp 164 | DocProject/Help/Html2 165 | DocProject/Help/html 166 | 167 | # Click-Once directory 168 | publish/ 169 | 170 | # Publish Web Output 171 | *.[Pp]ublish.xml 172 | *.azurePubxml 173 | # Note: Comment the next line if you want to checkin your web deploy settings, 174 | # but database connection strings (with potential passwords) will be unencrypted 175 | *.pubxml 176 | *.publishproj 177 | 178 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 179 | # checkin your Azure Web App publish settings, but sensitive information contained 180 | # in these scripts will be unencrypted 181 | PublishScripts/ 182 | 183 | # NuGet Packages 184 | *.nupkg 185 | # The packages folder can be ignored because of Package Restore 186 | **/[Pp]ackages/* 187 | # except build/, which is used as an MSBuild target. 188 | !**/[Pp]ackages/build/ 189 | # Uncomment if necessary however generally it will be regenerated when needed 190 | #!**/[Pp]ackages/repositories.config 191 | # NuGet v3's project.json files produces more ignorable files 192 | *.nuget.props 193 | *.nuget.targets 194 | 195 | # Microsoft Azure Build Output 196 | csx/ 197 | *.build.csdef 198 | 199 | # Microsoft Azure Emulator 200 | ecf/ 201 | rcf/ 202 | 203 | # Windows Store app package directories and files 204 | AppPackages/ 205 | BundleArtifacts/ 206 | Package.StoreAssociation.xml 207 | _pkginfo.txt 208 | *.appx 209 | 210 | # Visual Studio cache files 211 | # files ending in .cache can be ignored 212 | *.[Cc]ache 213 | # but keep track of directories ending in .cache 214 | !?*.[Cc]ache/ 215 | 216 | # Others 217 | ClientBin/ 218 | ~$* 219 | *~ 220 | *.dbmdl 221 | *.dbproj.schemaview 222 | *.jfm 223 | *.pfx 224 | *.publishsettings 225 | orleans.codegen.cs 226 | 227 | # Including strong name files can present a security risk 228 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 229 | #*.snk 230 | 231 | # Since there are multiple workflows, uncomment next line to ignore bower_components 232 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 233 | #bower_components/ 234 | 235 | # RIA/Silverlight projects 236 | Generated_Code/ 237 | 238 | # Backup & report files from converting an old project file 239 | # to a newer Visual Studio version. Backup files are not needed, 240 | # because we have git ;-) 241 | _UpgradeReport_Files/ 242 | Backup*/ 243 | UpgradeLog*.XML 244 | UpgradeLog*.htm 245 | ServiceFabricBackup/ 246 | *.rptproj.bak 247 | 248 | # SQL Server files 249 | *.mdf 250 | *.ldf 251 | *.ndf 252 | 253 | # Business Intelligence projects 254 | *.rdl.data 255 | *.bim.layout 256 | *.bim_*.settings 257 | *.rptproj.rsuser 258 | *- Backup*.rdl 259 | 260 | # Microsoft Fakes 261 | FakesAssemblies/ 262 | 263 | # GhostDoc plugin setting file 264 | *.GhostDoc.xml 265 | 266 | # Node.js Tools for Visual Studio 267 | .ntvs_analysis.dat 268 | node_modules/ 269 | 270 | # Visual Studio 6 build log 271 | *.plg 272 | 273 | # Visual Studio 6 workspace options file 274 | *.opt 275 | 276 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 277 | *.vbw 278 | 279 | # Visual Studio LightSwitch build output 280 | **/*.HTMLClient/GeneratedArtifacts 281 | **/*.DesktopClient/GeneratedArtifacts 282 | **/*.DesktopClient/ModelManifest.xml 283 | **/*.Server/GeneratedArtifacts 284 | **/*.Server/ModelManifest.xml 285 | _Pvt_Extensions 286 | 287 | # Paket dependency manager 288 | .paket/paket.exe 289 | paket-files/ 290 | 291 | # FAKE - F# Make 292 | .fake/ 293 | 294 | # JetBrains Rider 295 | .idea/ 296 | *.sln.iml 297 | 298 | # CodeRush personal settings 299 | .cr/personal 300 | 301 | # Python Tools for Visual Studio (PTVS) 302 | __pycache__/ 303 | *.pyc 304 | 305 | # Cake - Uncomment if you are using it 306 | # tools/** 307 | # !tools/packages.config 308 | 309 | # Tabs Studio 310 | *.tss 311 | 312 | # Telerik's JustMock configuration file 313 | *.jmconfig 314 | 315 | # BizTalk build output 316 | *.btp.cs 317 | *.btm.cs 318 | *.odx.cs 319 | *.xsd.cs 320 | 321 | # OpenCover UI analysis results 322 | OpenCover/ 323 | 324 | # Azure Stream Analytics local run output 325 | ASALocalRun/ 326 | 327 | # MSBuild Binary and Structured Log 328 | *.binlog 329 | 330 | # NVidia Nsight GPU debugger configuration file 331 | *.nvuser 332 | 333 | # MFractors (Xamarin productivity tool) working folder 334 | .mfractor/ 335 | 336 | # Local History for Visual Studio 337 | .localhistory/ 338 | 339 | # BeatPulse healthcheck temp database 340 | healthchecksdb -------------------------------------------------------------------------------- /src/PFire.Infrastructure/Migrations/20201219103931_FixTableNames.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | namespace PFire.Infrastructure.Migrations 5 | { 6 | public partial class FixTableNames : Migration 7 | { 8 | protected override void Up(MigrationBuilder migrationBuilder) 9 | { 10 | migrationBuilder.DropTable( 11 | name: "Friends"); 12 | 13 | migrationBuilder.DropTable( 14 | name: "Users"); 15 | 16 | migrationBuilder.CreateTable( 17 | name: "User", 18 | columns: table => new 19 | { 20 | Id = table.Column(type: "INTEGER", nullable: false) 21 | .Annotation("Sqlite:Autoincrement", true), 22 | Username = table.Column(type: "TEXT", maxLength: 1000, nullable: false), 23 | Password = table.Column(type: "TEXT", nullable: false), 24 | Salt = table.Column(type: "TEXT", nullable: false), 25 | Nickname = table.Column(type: "TEXT", maxLength: 1000, nullable: true), 26 | Version = table.Column(type: "BLOB", rowVersion: true, nullable: true), 27 | DateCreated = table.Column(type: "TEXT", nullable: false), 28 | DateModified = table.Column(type: "TEXT", nullable: false) 29 | }, 30 | constraints: table => 31 | { 32 | table.PrimaryKey("PK_User", x => x.Id); 33 | }); 34 | 35 | migrationBuilder.CreateTable( 36 | name: "Friend", 37 | columns: table => new 38 | { 39 | MeId = table.Column(type: "INTEGER", nullable: false), 40 | ThemId = table.Column(type: "INTEGER", nullable: false), 41 | Message = table.Column(type: "TEXT", maxLength: 1000, nullable: true), 42 | Pending = table.Column(type: "INTEGER", nullable: false), 43 | Version = table.Column(type: "BLOB", rowVersion: true, nullable: true), 44 | DateCreated = table.Column(type: "TEXT", nullable: false), 45 | DateModified = table.Column(type: "TEXT", nullable: false) 46 | }, 47 | constraints: table => 48 | { 49 | table.PrimaryKey("PK_Friend", x => new { x.MeId, x.ThemId }); 50 | table.ForeignKey( 51 | name: "FK_Friend_User_MeId", 52 | column: x => x.MeId, 53 | principalTable: "User", 54 | principalColumn: "Id", 55 | onDelete: ReferentialAction.Restrict); 56 | table.ForeignKey( 57 | name: "FK_Friend_User_ThemId", 58 | column: x => x.ThemId, 59 | principalTable: "User", 60 | principalColumn: "Id", 61 | onDelete: ReferentialAction.Restrict); 62 | }); 63 | 64 | migrationBuilder.CreateIndex( 65 | name: "IX_Friend_MeId_ThemId", 66 | table: "Friend", 67 | columns: new[] { "MeId", "ThemId" }, 68 | unique: true); 69 | 70 | migrationBuilder.CreateIndex( 71 | name: "IX_Friend_ThemId", 72 | table: "Friend", 73 | column: "ThemId"); 74 | 75 | migrationBuilder.CreateIndex( 76 | name: "IX_User_Id", 77 | table: "User", 78 | column: "Id"); 79 | 80 | migrationBuilder.CreateIndex( 81 | name: "IX_User_Username", 82 | table: "User", 83 | column: "Username"); 84 | } 85 | 86 | protected override void Down(MigrationBuilder migrationBuilder) 87 | { 88 | migrationBuilder.DropTable( 89 | name: "Friend"); 90 | 91 | migrationBuilder.DropTable( 92 | name: "User"); 93 | 94 | migrationBuilder.CreateTable( 95 | name: "Users", 96 | columns: table => new 97 | { 98 | Id = table.Column(type: "INTEGER", nullable: false) 99 | .Annotation("Sqlite:Autoincrement", true), 100 | DateCreated = table.Column(type: "TEXT", nullable: false), 101 | DateModified = table.Column(type: "TEXT", nullable: false), 102 | Nickname = table.Column(type: "TEXT", maxLength: 1000, nullable: true), 103 | Password = table.Column(type: "TEXT", nullable: false), 104 | Salt = table.Column(type: "TEXT", nullable: false), 105 | Username = table.Column(type: "TEXT", maxLength: 1000, nullable: false), 106 | Version = table.Column(type: "BLOB", rowVersion: true, nullable: true) 107 | }, 108 | constraints: table => 109 | { 110 | table.PrimaryKey("PK_Users", x => x.Id); 111 | }); 112 | 113 | migrationBuilder.CreateTable( 114 | name: "Friends", 115 | columns: table => new 116 | { 117 | MeId = table.Column(type: "INTEGER", nullable: false), 118 | ThemId = table.Column(type: "INTEGER", nullable: false), 119 | DateCreated = table.Column(type: "TEXT", nullable: false), 120 | DateModified = table.Column(type: "TEXT", nullable: false), 121 | Message = table.Column(type: "TEXT", maxLength: 1000, nullable: true), 122 | Pending = table.Column(type: "INTEGER", nullable: false), 123 | Version = table.Column(type: "BLOB", rowVersion: true, nullable: true) 124 | }, 125 | constraints: table => 126 | { 127 | table.PrimaryKey("PK_Friends", x => new { x.MeId, x.ThemId }); 128 | table.ForeignKey( 129 | name: "FK_Friends_Users_MeId", 130 | column: x => x.MeId, 131 | principalTable: "Users", 132 | principalColumn: "Id", 133 | onDelete: ReferentialAction.Restrict); 134 | table.ForeignKey( 135 | name: "FK_Friends_Users_ThemId", 136 | column: x => x.ThemId, 137 | principalTable: "Users", 138 | principalColumn: "Id", 139 | onDelete: ReferentialAction.Restrict); 140 | }); 141 | 142 | migrationBuilder.CreateIndex( 143 | name: "IX_Friends_MeId_ThemId", 144 | table: "Friends", 145 | columns: new[] { "MeId", "ThemId" }, 146 | unique: true); 147 | 148 | migrationBuilder.CreateIndex( 149 | name: "IX_Friends_ThemId", 150 | table: "Friends", 151 | column: "ThemId"); 152 | 153 | migrationBuilder.CreateIndex( 154 | name: "IX_Users_Id", 155 | table: "Users", 156 | column: "Id"); 157 | 158 | migrationBuilder.CreateIndex( 159 | name: "IX_Users_Username", 160 | table: "Users", 161 | column: "Username"); 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/PFire.Infrastructure/Migrations/DatabaseContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 6 | using PFire.Infrastructure.Services; 7 | 8 | #nullable disable 9 | 10 | namespace PFire.Infrastructure.Migrations 11 | { 12 | [DbContext(typeof(DatabaseContext))] 13 | partial class DatabaseContextModelSnapshot : ModelSnapshot 14 | { 15 | protected override void BuildModel(ModelBuilder modelBuilder) 16 | { 17 | #pragma warning disable 612, 618 18 | modelBuilder.HasAnnotation("ProductVersion", "9.0.1"); 19 | 20 | modelBuilder.Entity("PFire.Infrastructure.Entities.ClientPreferences", b => 21 | { 22 | b.Property("Id") 23 | .ValueGeneratedOnAdd() 24 | .HasColumnType("INTEGER"); 25 | 26 | b.Property("ChatShowTimestamps") 27 | .HasColumnType("INTEGER"); 28 | 29 | b.Property("DateCreated") 30 | .HasColumnType("TEXT"); 31 | 32 | b.Property("DateModified") 33 | .HasColumnType("TEXT"); 34 | 35 | b.Property("GameStatusShowFriendOfFriends") 36 | .HasColumnType("INTEGER"); 37 | 38 | b.Property("GameStatusShowMyFriends") 39 | .HasColumnType("INTEGER"); 40 | 41 | b.Property("GameStatusShowMyGameServer") 42 | .HasColumnType("INTEGER"); 43 | 44 | b.Property("GameStatusShowMyProfile") 45 | .HasColumnType("INTEGER"); 46 | 47 | b.Property("NotificationConnectionStateChanges") 48 | .HasColumnType("INTEGER"); 49 | 50 | b.Property("NotificationDownloadStartsFinishes") 51 | .HasColumnType("INTEGER"); 52 | 53 | b.Property("NotificationFriendLogsOnOff") 54 | .HasColumnType("INTEGER"); 55 | 56 | b.Property("PlaySoundFriendLogsOnOff") 57 | .HasColumnType("INTEGER"); 58 | 59 | b.Property("PlaySoundReceiveMessageWhileGaming") 60 | .HasColumnType("INTEGER"); 61 | 62 | b.Property("PlaySoundScreenshotWhileGaming") 63 | .HasColumnType("INTEGER"); 64 | 65 | b.Property("PlaySoundSendOrReceiveMessage") 66 | .HasColumnType("INTEGER"); 67 | 68 | b.Property("PlaySoundSendReceiveVoiceChatRequest") 69 | .HasColumnType("INTEGER"); 70 | 71 | b.Property("PlaySoundSomeoneJoinsLeaveChatroom") 72 | .HasColumnType("INTEGER"); 73 | 74 | b.Property("ShowNicknames") 75 | .HasColumnType("INTEGER"); 76 | 77 | b.Property("ShowOfflineFriends") 78 | .HasColumnType("INTEGER"); 79 | 80 | b.Property("ShowVoiceChatServerToFriends") 81 | .HasColumnType("INTEGER"); 82 | 83 | b.Property("ShowWhenTyping") 84 | .HasColumnType("INTEGER"); 85 | 86 | b.Property("UserId") 87 | .HasColumnType("INTEGER"); 88 | 89 | b.Property("Version") 90 | .HasColumnType("BLOB"); 91 | 92 | b.HasKey("Id"); 93 | 94 | b.HasIndex("UserId") 95 | .IsUnique(); 96 | 97 | b.ToTable("ClientPreferences"); 98 | }); 99 | 100 | modelBuilder.Entity("PFire.Infrastructure.Entities.Friend", b => 101 | { 102 | b.Property("MeId") 103 | .HasColumnType("INTEGER"); 104 | 105 | b.Property("ThemId") 106 | .HasColumnType("INTEGER"); 107 | 108 | b.Property("DateCreated") 109 | .HasColumnType("TEXT"); 110 | 111 | b.Property("DateModified") 112 | .HasColumnType("TEXT"); 113 | 114 | b.Property("Message") 115 | .HasMaxLength(1000) 116 | .HasColumnType("TEXT"); 117 | 118 | b.Property("Pending") 119 | .HasColumnType("INTEGER"); 120 | 121 | b.Property("Version") 122 | .IsConcurrencyToken() 123 | .ValueGeneratedOnAddOrUpdate() 124 | .HasColumnType("BLOB"); 125 | 126 | b.HasKey("MeId", "ThemId"); 127 | 128 | b.HasIndex("ThemId"); 129 | 130 | b.HasIndex("MeId", "ThemId") 131 | .IsUnique(); 132 | 133 | b.ToTable("Friend", (string)null); 134 | }); 135 | 136 | modelBuilder.Entity("PFire.Infrastructure.Entities.Group", b => 137 | { 138 | b.Property("Id") 139 | .ValueGeneratedOnAdd() 140 | .HasColumnType("INTEGER"); 141 | 142 | b.Property("DateCreated") 143 | .HasColumnType("TEXT"); 144 | 145 | b.Property("DateModified") 146 | .HasColumnType("TEXT"); 147 | 148 | b.Property("Name") 149 | .IsRequired() 150 | .HasMaxLength(1000) 151 | .HasColumnType("TEXT"); 152 | 153 | b.Property("OwnerId") 154 | .HasColumnType("INTEGER"); 155 | 156 | b.Property("Version") 157 | .IsConcurrencyToken() 158 | .ValueGeneratedOnAddOrUpdate() 159 | .HasColumnType("BLOB"); 160 | 161 | b.HasKey("Id"); 162 | 163 | b.HasIndex("OwnerId"); 164 | 165 | b.ToTable("Group"); 166 | }); 167 | 168 | modelBuilder.Entity("PFire.Infrastructure.Entities.User", b => 169 | { 170 | b.Property("Id") 171 | .ValueGeneratedOnAdd() 172 | .HasColumnType("INTEGER"); 173 | 174 | b.Property("DateCreated") 175 | .HasColumnType("TEXT"); 176 | 177 | b.Property("DateModified") 178 | .HasColumnType("TEXT"); 179 | 180 | b.Property("Nickname") 181 | .HasMaxLength(1000) 182 | .HasColumnType("TEXT"); 183 | 184 | b.Property("Password") 185 | .IsRequired() 186 | .HasColumnType("TEXT"); 187 | 188 | b.Property("Salt") 189 | .IsRequired() 190 | .HasColumnType("TEXT"); 191 | 192 | b.Property("Username") 193 | .IsRequired() 194 | .HasMaxLength(1000) 195 | .HasColumnType("TEXT"); 196 | 197 | b.Property("Version") 198 | .IsConcurrencyToken() 199 | .ValueGeneratedOnAddOrUpdate() 200 | .HasColumnType("BLOB"); 201 | 202 | b.HasKey("Id"); 203 | 204 | b.HasIndex("Id"); 205 | 206 | b.HasIndex("Username"); 207 | 208 | b.ToTable("User", (string)null); 209 | }); 210 | 211 | modelBuilder.Entity("PFire.Infrastructure.Entities.ClientPreferences", b => 212 | { 213 | b.HasOne("PFire.Infrastructure.Entities.User", "User") 214 | .WithOne("ClientPreferences") 215 | .HasForeignKey("PFire.Infrastructure.Entities.ClientPreferences", "UserId") 216 | .OnDelete(DeleteBehavior.Cascade) 217 | .IsRequired(); 218 | 219 | b.Navigation("User"); 220 | }); 221 | 222 | modelBuilder.Entity("PFire.Infrastructure.Entities.Friend", b => 223 | { 224 | b.HasOne("PFire.Infrastructure.Entities.User", "Me") 225 | .WithMany("MyFriends") 226 | .HasForeignKey("MeId") 227 | .OnDelete(DeleteBehavior.Restrict) 228 | .IsRequired(); 229 | 230 | b.HasOne("PFire.Infrastructure.Entities.User", "Them") 231 | .WithMany("FriendsOf") 232 | .HasForeignKey("ThemId") 233 | .OnDelete(DeleteBehavior.Restrict) 234 | .IsRequired(); 235 | 236 | b.Navigation("Me"); 237 | 238 | b.Navigation("Them"); 239 | }); 240 | 241 | modelBuilder.Entity("PFire.Infrastructure.Entities.Group", b => 242 | { 243 | b.HasOne("PFire.Infrastructure.Entities.User", "Owner") 244 | .WithMany() 245 | .HasForeignKey("OwnerId") 246 | .OnDelete(DeleteBehavior.Cascade) 247 | .IsRequired(); 248 | 249 | b.Navigation("Owner"); 250 | }); 251 | 252 | modelBuilder.Entity("PFire.Infrastructure.Entities.User", b => 253 | { 254 | b.Navigation("ClientPreferences"); 255 | 256 | b.Navigation("FriendsOf"); 257 | 258 | b.Navigation("MyFriends"); 259 | }); 260 | #pragma warning restore 612, 618 261 | } 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /src/PFire.Infrastructure/Migrations/20250307032454_ClientPreferences.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Migrations; 6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 7 | using PFire.Infrastructure.Services; 8 | 9 | #nullable disable 10 | 11 | namespace PFire.Infrastructure.Migrations 12 | { 13 | [DbContext(typeof(DatabaseContext))] 14 | [Migration("20250307032454_ClientPreferences")] 15 | partial class ClientPreferences 16 | { 17 | /// 18 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 19 | { 20 | #pragma warning disable 612, 618 21 | modelBuilder.HasAnnotation("ProductVersion", "9.0.1"); 22 | 23 | modelBuilder.Entity("PFire.Infrastructure.Entities.ClientPreferences", b => 24 | { 25 | b.Property("Id") 26 | .ValueGeneratedOnAdd() 27 | .HasColumnType("INTEGER"); 28 | 29 | b.Property("ChatShowTimestamps") 30 | .HasColumnType("INTEGER"); 31 | 32 | b.Property("DateCreated") 33 | .HasColumnType("TEXT"); 34 | 35 | b.Property("DateModified") 36 | .HasColumnType("TEXT"); 37 | 38 | b.Property("GameStatusShowFriendOfFriends") 39 | .HasColumnType("INTEGER"); 40 | 41 | b.Property("GameStatusShowMyFriends") 42 | .HasColumnType("INTEGER"); 43 | 44 | b.Property("GameStatusShowMyGameServer") 45 | .HasColumnType("INTEGER"); 46 | 47 | b.Property("GameStatusShowMyProfile") 48 | .HasColumnType("INTEGER"); 49 | 50 | b.Property("NotificationConnectionStateChanges") 51 | .HasColumnType("INTEGER"); 52 | 53 | b.Property("NotificationDownloadStartsFinishes") 54 | .HasColumnType("INTEGER"); 55 | 56 | b.Property("NotificationFriendLogsOnOff") 57 | .HasColumnType("INTEGER"); 58 | 59 | b.Property("PlaySoundFriendLogsOnOff") 60 | .HasColumnType("INTEGER"); 61 | 62 | b.Property("PlaySoundReceiveMessageWhileGaming") 63 | .HasColumnType("INTEGER"); 64 | 65 | b.Property("PlaySoundScreenshotWhileGaming") 66 | .HasColumnType("INTEGER"); 67 | 68 | b.Property("PlaySoundSendOrReceiveMessage") 69 | .HasColumnType("INTEGER"); 70 | 71 | b.Property("PlaySoundSendReceiveVoiceChatRequest") 72 | .HasColumnType("INTEGER"); 73 | 74 | b.Property("PlaySoundSomeoneJoinsLeaveChatroom") 75 | .HasColumnType("INTEGER"); 76 | 77 | b.Property("ShowNicknames") 78 | .HasColumnType("INTEGER"); 79 | 80 | b.Property("ShowOfflineFriends") 81 | .HasColumnType("INTEGER"); 82 | 83 | b.Property("ShowVoiceChatServerToFriends") 84 | .HasColumnType("INTEGER"); 85 | 86 | b.Property("ShowWhenTyping") 87 | .HasColumnType("INTEGER"); 88 | 89 | b.Property("UserId") 90 | .HasColumnType("INTEGER"); 91 | 92 | b.Property("Version") 93 | .HasColumnType("BLOB"); 94 | 95 | b.HasKey("Id"); 96 | 97 | b.HasIndex("UserId") 98 | .IsUnique(); 99 | 100 | b.ToTable("ClientPreferences"); 101 | }); 102 | 103 | modelBuilder.Entity("PFire.Infrastructure.Entities.Friend", b => 104 | { 105 | b.Property("MeId") 106 | .HasColumnType("INTEGER"); 107 | 108 | b.Property("ThemId") 109 | .HasColumnType("INTEGER"); 110 | 111 | b.Property("DateCreated") 112 | .HasColumnType("TEXT"); 113 | 114 | b.Property("DateModified") 115 | .HasColumnType("TEXT"); 116 | 117 | b.Property("Message") 118 | .HasMaxLength(1000) 119 | .HasColumnType("TEXT"); 120 | 121 | b.Property("Pending") 122 | .HasColumnType("INTEGER"); 123 | 124 | b.Property("Version") 125 | .IsConcurrencyToken() 126 | .ValueGeneratedOnAddOrUpdate() 127 | .HasColumnType("BLOB"); 128 | 129 | b.HasKey("MeId", "ThemId"); 130 | 131 | b.HasIndex("ThemId"); 132 | 133 | b.HasIndex("MeId", "ThemId") 134 | .IsUnique(); 135 | 136 | b.ToTable("Friend", (string)null); 137 | }); 138 | 139 | modelBuilder.Entity("PFire.Infrastructure.Entities.Group", b => 140 | { 141 | b.Property("Id") 142 | .ValueGeneratedOnAdd() 143 | .HasColumnType("INTEGER"); 144 | 145 | b.Property("DateCreated") 146 | .HasColumnType("TEXT"); 147 | 148 | b.Property("DateModified") 149 | .HasColumnType("TEXT"); 150 | 151 | b.Property("Name") 152 | .IsRequired() 153 | .HasMaxLength(1000) 154 | .HasColumnType("TEXT"); 155 | 156 | b.Property("OwnerId") 157 | .HasColumnType("INTEGER"); 158 | 159 | b.Property("Version") 160 | .IsConcurrencyToken() 161 | .ValueGeneratedOnAddOrUpdate() 162 | .HasColumnType("BLOB"); 163 | 164 | b.HasKey("Id"); 165 | 166 | b.HasIndex("OwnerId"); 167 | 168 | b.ToTable("Group"); 169 | }); 170 | 171 | modelBuilder.Entity("PFire.Infrastructure.Entities.User", b => 172 | { 173 | b.Property("Id") 174 | .ValueGeneratedOnAdd() 175 | .HasColumnType("INTEGER"); 176 | 177 | b.Property("DateCreated") 178 | .HasColumnType("TEXT"); 179 | 180 | b.Property("DateModified") 181 | .HasColumnType("TEXT"); 182 | 183 | b.Property("Nickname") 184 | .HasMaxLength(1000) 185 | .HasColumnType("TEXT"); 186 | 187 | b.Property("Password") 188 | .IsRequired() 189 | .HasColumnType("TEXT"); 190 | 191 | b.Property("Salt") 192 | .IsRequired() 193 | .HasColumnType("TEXT"); 194 | 195 | b.Property("Username") 196 | .IsRequired() 197 | .HasMaxLength(1000) 198 | .HasColumnType("TEXT"); 199 | 200 | b.Property("Version") 201 | .IsConcurrencyToken() 202 | .ValueGeneratedOnAddOrUpdate() 203 | .HasColumnType("BLOB"); 204 | 205 | b.HasKey("Id"); 206 | 207 | b.HasIndex("Id"); 208 | 209 | b.HasIndex("Username"); 210 | 211 | b.ToTable("User", (string)null); 212 | }); 213 | 214 | modelBuilder.Entity("PFire.Infrastructure.Entities.ClientPreferences", b => 215 | { 216 | b.HasOne("PFire.Infrastructure.Entities.User", "User") 217 | .WithOne("ClientPreferences") 218 | .HasForeignKey("PFire.Infrastructure.Entities.ClientPreferences", "UserId") 219 | .OnDelete(DeleteBehavior.Cascade) 220 | .IsRequired(); 221 | 222 | b.Navigation("User"); 223 | }); 224 | 225 | modelBuilder.Entity("PFire.Infrastructure.Entities.Friend", b => 226 | { 227 | b.HasOne("PFire.Infrastructure.Entities.User", "Me") 228 | .WithMany("MyFriends") 229 | .HasForeignKey("MeId") 230 | .OnDelete(DeleteBehavior.Restrict) 231 | .IsRequired(); 232 | 233 | b.HasOne("PFire.Infrastructure.Entities.User", "Them") 234 | .WithMany("FriendsOf") 235 | .HasForeignKey("ThemId") 236 | .OnDelete(DeleteBehavior.Restrict) 237 | .IsRequired(); 238 | 239 | b.Navigation("Me"); 240 | 241 | b.Navigation("Them"); 242 | }); 243 | 244 | modelBuilder.Entity("PFire.Infrastructure.Entities.Group", b => 245 | { 246 | b.HasOne("PFire.Infrastructure.Entities.User", "Owner") 247 | .WithMany() 248 | .HasForeignKey("OwnerId") 249 | .OnDelete(DeleteBehavior.Cascade) 250 | .IsRequired(); 251 | 252 | b.Navigation("Owner"); 253 | }); 254 | 255 | modelBuilder.Entity("PFire.Infrastructure.Entities.User", b => 256 | { 257 | b.Navigation("ClientPreferences"); 258 | 259 | b.Navigation("FriendsOf"); 260 | 261 | b.Navigation("MyFriends"); 262 | }); 263 | #pragma warning restore 612, 618 264 | } 265 | } 266 | } 267 | --------------------------------------------------------------------------------