From 46649adfd8761d47830bd9e4d19175b8fc55dafd Mon Sep 17 00:00:00 2001 From: Timothy Schenk Date: Wed, 15 Nov 2023 20:00:08 +0100 Subject: [PATCH] feat: character creation works requires base stats and parsing of values provided by user --- Server/DB/Documents/Account.cs | 18 +- Server/DB/Documents/Character.cs | 15 +- Server/DB/Documents/Guild.cs | 8 +- Server/DB/Documents/GuildMember.cs | 8 +- Server/DB/Documents/InventoryItem.cs | 2 +- .../{ItemType.cs => InventoryTab.cs} | 2 +- ...31115174714_GuildIsNotRequired.Designer.cs | 333 ++++++++++++++++++ .../20231115174714_GuildIsNotRequired.cs | 27 ++ ...183824_SwitchToDataAnnotations.Designer.cs | 322 +++++++++++++++++ .../20231115183824_SwitchToDataAnnotations.cs | 230 ++++++++++++ .../WonderkingContextModelSnapshot.cs | 41 +-- Server/DB/WonderkingContext.cs | 26 -- .../PacketHandlers/ChannelSelectionHandler.cs | 8 +- .../CharacterCreationHandler..cs | 89 +++++ .../CharacterNameCheckHandler.cs | 25 ++ Server/Program.cs | 3 +- Server/Services/ItemObjectPoolService.cs | 42 ++- Server/docker-compose.yml | 4 +- Server/settings.Development.json | 2 +- Wonderking/Game/DataReader.cs | 4 +- Wonderking/Game/Reader/ItemReader.cs | 12 +- .../Incoming/CharacterCreationPacket.cs | 39 ++ .../Incoming/CharacterNameCheckPacket.cs | 19 + Wonderking/Packets/OperationCode.cs | 6 + .../CharacterCreationResponsePacket.cs | 58 +++ .../CharacterNameCheckPacketResponse.cs | 19 + Wonderking/Packets/Outgoing/Data/BaseStats.cs | 2 + Wonderking/Packets/Outgoing/Data/JobData.cs | 2 + Wonderking/Wonderking.csproj | 1 + 29 files changed, 1276 insertions(+), 91 deletions(-) rename Server/DB/Documents/{ItemType.cs => InventoryTab.cs} (83%) create mode 100644 Server/DB/Migrations/20231115174714_GuildIsNotRequired.Designer.cs create mode 100644 Server/DB/Migrations/20231115174714_GuildIsNotRequired.cs create mode 100644 Server/DB/Migrations/20231115183824_SwitchToDataAnnotations.Designer.cs create mode 100644 Server/DB/Migrations/20231115183824_SwitchToDataAnnotations.cs create mode 100644 Server/PacketHandlers/CharacterCreationHandler..cs create mode 100644 Server/PacketHandlers/CharacterNameCheckHandler.cs create mode 100644 Wonderking/Packets/Incoming/CharacterCreationPacket.cs create mode 100644 Wonderking/Packets/Incoming/CharacterNameCheckPacket.cs create mode 100644 Wonderking/Packets/Outgoing/CharacterCreationResponsePacket.cs create mode 100644 Wonderking/Packets/Outgoing/CharacterNameCheckPacketResponse.cs diff --git a/Server/DB/Documents/Account.cs b/Server/DB/Documents/Account.cs index 7d66d36..20c8bc0 100644 --- a/Server/DB/Documents/Account.cs +++ b/Server/DB/Documents/Account.cs @@ -1,5 +1,10 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Microsoft.EntityFrameworkCore; + namespace Server.DB.Documents; +[Index(nameof(Username), IsUnique = true), Index(nameof(Id), IsUnique = true)] public class Account { public Account(string username, byte[] password, string email, byte permissionLevel, byte[] salt) @@ -11,12 +16,15 @@ public class Account this.Salt = salt; } + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public Guid Id { get; set; } - public string Username { get; set; } - public byte[] Password { get; set; } - public string Email { get; set; } + [Column(TypeName = "varchar(20)")] public string Username { get; set; } + [Column(TypeName = "bytea")] public byte[] Password { get; set; } + + [EmailAddress] public string Email { get; set; } public byte PermissionLevel { get; set; } - public byte[] Salt { get; set; } - public ICollection Characters { get; } = new List(); + [Column(TypeName = "bytea")] public byte[] Salt { get; set; } + public virtual ICollection Characters { get; } = new List(); } diff --git a/Server/DB/Documents/Character.cs b/Server/DB/Documents/Character.cs index bb507d5..c286fff 100644 --- a/Server/DB/Documents/Character.cs +++ b/Server/DB/Documents/Character.cs @@ -1,3 +1,5 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; using Wonderking.Game.Data.Character; using Wonderking.Packets.Outgoing.Data; @@ -5,10 +7,12 @@ namespace Server.DB.Documents; public class Character { - public byte ServerId { get; set; } - public Guid AccountId { get; set; } - public Account Account { get; set; } + public virtual Account Account { get; set; } + + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public Guid Id { get; set; } + public ushort MapId { get; set; } public string Name { get; set; } public short LastXCoordinate { get; set; } @@ -17,13 +21,12 @@ public class Character public Gender Gender { get; set; } public long Experience { get; set; } public byte Level { get; set; } - public ICollection InventoryItems { get; set; } + public virtual ICollection InventoryItems { get; set; } public BaseStats BaseStats { get; set; } public JobData JobData { get; set; } public int Health { get; set; } public int Mana { get; set; } - public Guid GuildId { get; set; } - public Guild Guild { get; set; } + public virtual Guild Guild { get; set; } } diff --git a/Server/DB/Documents/Guild.cs b/Server/DB/Documents/Guild.cs index a907566..3109d00 100644 --- a/Server/DB/Documents/Guild.cs +++ b/Server/DB/Documents/Guild.cs @@ -1,9 +1,15 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + namespace Server.DB.Documents; public class Guild { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public Guid Id { get; set; } + public string Name { get; set; } public string Notice { get; set; } - public ICollection GuildMembers { get; set; } + public virtual ICollection GuildMembers { get; set; } } diff --git a/Server/DB/Documents/GuildMember.cs b/Server/DB/Documents/GuildMember.cs index 44cb91b..557aa2d 100644 --- a/Server/DB/Documents/GuildMember.cs +++ b/Server/DB/Documents/GuildMember.cs @@ -1,11 +1,15 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + namespace Server.DB.Documents; public class GuildMember { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public Guid Id { get; set; } - public Guid CharacterId { get; set; } + public Character Character { get; set; } - public Guid GuildId { get; set; } public Guild Guild { get; set; } public GuildRank Rank { get; set; } } diff --git a/Server/DB/Documents/InventoryItem.cs b/Server/DB/Documents/InventoryItem.cs index 58ddd55..08f5078 100644 --- a/Server/DB/Documents/InventoryItem.cs +++ b/Server/DB/Documents/InventoryItem.cs @@ -8,7 +8,7 @@ public class InventoryItem public ushort ItemId { get; set; } public ushort Count { get; set; } public byte Slot { get; set; } - public ItemType ItemType { get; set; } + public InventoryTab InventoryTab { get; set; } public byte Level { get; set; } public byte Rarity { get; set; } public byte AddOption { get; set; } diff --git a/Server/DB/Documents/ItemType.cs b/Server/DB/Documents/InventoryTab.cs similarity index 83% rename from Server/DB/Documents/ItemType.cs rename to Server/DB/Documents/InventoryTab.cs index 2b12c16..cbcdae1 100644 --- a/Server/DB/Documents/ItemType.cs +++ b/Server/DB/Documents/InventoryTab.cs @@ -1,6 +1,6 @@ namespace Server.DB.Documents; -public enum ItemType : byte +public enum InventoryTab : byte { WornEquipment = 0, WornCashEquipment = 1, diff --git a/Server/DB/Migrations/20231115174714_GuildIsNotRequired.Designer.cs b/Server/DB/Migrations/20231115174714_GuildIsNotRequired.Designer.cs new file mode 100644 index 0000000..c5b763c --- /dev/null +++ b/Server/DB/Migrations/20231115174714_GuildIsNotRequired.Designer.cs @@ -0,0 +1,333 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Server.DB; + +#nullable disable + +namespace Server.DB.Migrations +{ + [DbContext(typeof(WonderkingContext))] + [Migration("20231115174714_GuildIsNotRequired")] + partial class GuildIsNotRequired + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Server.DB.Documents.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("Password") + .HasColumnType("bytea"); + + b.Property("PermissionLevel") + .HasColumnType("smallint"); + + b.Property("Salt") + .HasColumnType("bytea"); + + b.Property("Username") + .HasColumnType("varchar(20)"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("Server.DB.Documents.Character", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("Experience") + .HasColumnType("bigint"); + + b.Property("Gender") + .HasColumnType("smallint"); + + b.Property("GuildId") + .HasColumnType("uuid"); + + b.Property("Health") + .HasColumnType("integer"); + + b.Property("LastXCoordinate") + .HasColumnType("smallint"); + + b.Property("LastYCoordinate") + .HasColumnType("smallint"); + + b.Property("Level") + .HasColumnType("smallint"); + + b.Property("Mana") + .HasColumnType("integer"); + + b.Property("MapId") + .HasColumnType("integer"); + + b.Property("Name") + .HasColumnType("varchar(20)"); + + b.Property("PvPLevel") + .HasColumnType("smallint"); + + b.Property("ServerId") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("GuildId"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Characters"); + }); + + modelBuilder.Entity("Server.DB.Documents.Guild", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Notice") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Guild"); + }); + + modelBuilder.Entity("Server.DB.Documents.GuildMember", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CharacterId") + .HasColumnType("uuid"); + + b.Property("GuildId") + .HasColumnType("uuid"); + + b.Property("Rank") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("CharacterId"); + + b.HasIndex("GuildId"); + + b.ToTable("GuildMember"); + }); + + modelBuilder.Entity("Server.DB.Documents.InventoryItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AddOption") + .HasColumnType("smallint"); + + b.Property("AddOption2") + .HasColumnType("smallint"); + + b.Property("AddOption3") + .HasColumnType("smallint"); + + b.Property("CharacterId") + .HasColumnType("uuid"); + + b.Property("Count") + .HasColumnType("integer"); + + b.Property("InventoryTab") + .HasColumnType("smallint"); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("Level") + .HasColumnType("smallint"); + + b.Property("Option") + .HasColumnType("smallint"); + + b.Property("Option2") + .HasColumnType("smallint"); + + b.Property("Option3") + .HasColumnType("smallint"); + + b.Property("Rarity") + .HasColumnType("smallint"); + + b.Property("Slot") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("CharacterId"); + + b.ToTable("InventoryItem"); + }); + + modelBuilder.Entity("Server.DB.Documents.Character", b => + { + b.HasOne("Server.DB.Documents.Account", "Account") + .WithMany("Characters") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Server.DB.Documents.Guild", "Guild") + .WithMany() + .HasForeignKey("GuildId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("Wonderking.Packets.Outgoing.Data.BaseStats", "BaseStats", b1 => + { + b1.Property("CharacterId") + .HasColumnType("uuid"); + + b1.Property("Dexterity") + .HasColumnType("smallint"); + + b1.Property("Intelligence") + .HasColumnType("smallint"); + + b1.Property("Luck") + .HasColumnType("smallint"); + + b1.Property("Strength") + .HasColumnType("smallint"); + + b1.Property("Vitality") + .HasColumnType("smallint"); + + b1.Property("Wisdom") + .HasColumnType("smallint"); + + b1.HasKey("CharacterId"); + + b1.ToTable("Characters"); + + b1.WithOwner() + .HasForeignKey("CharacterId"); + }); + + b.OwnsOne("Wonderking.Packets.Outgoing.Data.JobData", "JobData", b1 => + { + b1.Property("CharacterId") + .HasColumnType("uuid"); + + b1.Property("FirstJob") + .HasColumnType("smallint"); + + b1.Property("FourthJob") + .HasColumnType("smallint"); + + b1.Property("SecondJob") + .HasColumnType("smallint"); + + b1.Property("ThirdJob") + .HasColumnType("smallint"); + + b1.HasKey("CharacterId"); + + b1.ToTable("Characters"); + + b1.WithOwner() + .HasForeignKey("CharacterId"); + }); + + b.Navigation("Account"); + + b.Navigation("BaseStats"); + + b.Navigation("Guild"); + + b.Navigation("JobData"); + }); + + modelBuilder.Entity("Server.DB.Documents.GuildMember", b => + { + b.HasOne("Server.DB.Documents.Character", "Character") + .WithMany() + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Server.DB.Documents.Guild", "Guild") + .WithMany("GuildMembers") + .HasForeignKey("GuildId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Character"); + + b.Navigation("Guild"); + }); + + modelBuilder.Entity("Server.DB.Documents.InventoryItem", b => + { + b.HasOne("Server.DB.Documents.Character", "Character") + .WithMany("InventoryItems") + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Character"); + }); + + modelBuilder.Entity("Server.DB.Documents.Account", b => + { + b.Navigation("Characters"); + }); + + modelBuilder.Entity("Server.DB.Documents.Character", b => + { + b.Navigation("InventoryItems"); + }); + + modelBuilder.Entity("Server.DB.Documents.Guild", b => + { + b.Navigation("GuildMembers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Server/DB/Migrations/20231115174714_GuildIsNotRequired.cs b/Server/DB/Migrations/20231115174714_GuildIsNotRequired.cs new file mode 100644 index 0000000..27c1de3 --- /dev/null +++ b/Server/DB/Migrations/20231115174714_GuildIsNotRequired.cs @@ -0,0 +1,27 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Server.DB.Migrations; + +/// +public partial class GuildIsNotRequired : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "ItemType", + table: "InventoryItem", + newName: "InventoryTab"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "InventoryTab", + table: "InventoryItem", + newName: "ItemType"); + } +} diff --git a/Server/DB/Migrations/20231115183824_SwitchToDataAnnotations.Designer.cs b/Server/DB/Migrations/20231115183824_SwitchToDataAnnotations.Designer.cs new file mode 100644 index 0000000..907419e --- /dev/null +++ b/Server/DB/Migrations/20231115183824_SwitchToDataAnnotations.Designer.cs @@ -0,0 +1,322 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Server.DB; + +#nullable disable + +namespace Server.DB.Migrations +{ + [DbContext(typeof(WonderkingContext))] + [Migration("20231115183824_SwitchToDataAnnotations")] + partial class SwitchToDataAnnotations + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Server.DB.Documents.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("Password") + .HasColumnType("bytea"); + + b.Property("PermissionLevel") + .HasColumnType("smallint"); + + b.Property("Salt") + .HasColumnType("bytea"); + + b.Property("Username") + .HasColumnType("varchar(20)"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .IsUnique(); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("Server.DB.Documents.Character", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("uuid"); + + b.Property("Experience") + .HasColumnType("bigint"); + + b.Property("Gender") + .HasColumnType("smallint"); + + b.Property("GuildId") + .HasColumnType("uuid"); + + b.Property("Health") + .HasColumnType("integer"); + + b.Property("LastXCoordinate") + .HasColumnType("smallint"); + + b.Property("LastYCoordinate") + .HasColumnType("smallint"); + + b.Property("Level") + .HasColumnType("smallint"); + + b.Property("Mana") + .HasColumnType("integer"); + + b.Property("MapId") + .HasColumnType("integer"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("PvPLevel") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("GuildId"); + + b.ToTable("Characters"); + }); + + modelBuilder.Entity("Server.DB.Documents.Guild", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Notice") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Guild"); + }); + + modelBuilder.Entity("Server.DB.Documents.GuildMember", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CharacterId") + .HasColumnType("uuid"); + + b.Property("GuildId") + .HasColumnType("uuid"); + + b.Property("Rank") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("CharacterId"); + + b.HasIndex("GuildId"); + + b.ToTable("GuildMember"); + }); + + modelBuilder.Entity("Server.DB.Documents.InventoryItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AddOption") + .HasColumnType("smallint"); + + b.Property("AddOption2") + .HasColumnType("smallint"); + + b.Property("AddOption3") + .HasColumnType("smallint"); + + b.Property("CharacterId") + .HasColumnType("uuid"); + + b.Property("Count") + .HasColumnType("integer"); + + b.Property("InventoryTab") + .HasColumnType("smallint"); + + b.Property("ItemId") + .HasColumnType("integer"); + + b.Property("Level") + .HasColumnType("smallint"); + + b.Property("Option") + .HasColumnType("smallint"); + + b.Property("Option2") + .HasColumnType("smallint"); + + b.Property("Option3") + .HasColumnType("smallint"); + + b.Property("Rarity") + .HasColumnType("smallint"); + + b.Property("Slot") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("CharacterId"); + + b.ToTable("InventoryItem"); + }); + + modelBuilder.Entity("Server.DB.Documents.Character", b => + { + b.HasOne("Server.DB.Documents.Account", "Account") + .WithMany("Characters") + .HasForeignKey("AccountId"); + + b.HasOne("Server.DB.Documents.Guild", "Guild") + .WithMany() + .HasForeignKey("GuildId"); + + b.OwnsOne("Wonderking.Packets.Outgoing.Data.BaseStats", "BaseStats", b1 => + { + b1.Property("CharacterId") + .HasColumnType("uuid"); + + b1.Property("Dexterity") + .HasColumnType("smallint"); + + b1.Property("Intelligence") + .HasColumnType("smallint"); + + b1.Property("Luck") + .HasColumnType("smallint"); + + b1.Property("Strength") + .HasColumnType("smallint"); + + b1.Property("Vitality") + .HasColumnType("smallint"); + + b1.Property("Wisdom") + .HasColumnType("smallint"); + + b1.HasKey("CharacterId"); + + b1.ToTable("Characters"); + + b1.WithOwner() + .HasForeignKey("CharacterId"); + }); + + b.OwnsOne("Wonderking.Packets.Outgoing.Data.JobData", "JobData", b1 => + { + b1.Property("CharacterId") + .HasColumnType("uuid"); + + b1.Property("FirstJob") + .HasColumnType("smallint"); + + b1.Property("FourthJob") + .HasColumnType("smallint"); + + b1.Property("SecondJob") + .HasColumnType("smallint"); + + b1.Property("ThirdJob") + .HasColumnType("smallint"); + + b1.HasKey("CharacterId"); + + b1.ToTable("Characters"); + + b1.WithOwner() + .HasForeignKey("CharacterId"); + }); + + b.Navigation("Account"); + + b.Navigation("BaseStats"); + + b.Navigation("Guild"); + + b.Navigation("JobData"); + }); + + modelBuilder.Entity("Server.DB.Documents.GuildMember", b => + { + b.HasOne("Server.DB.Documents.Character", "Character") + .WithMany() + .HasForeignKey("CharacterId"); + + b.HasOne("Server.DB.Documents.Guild", "Guild") + .WithMany("GuildMembers") + .HasForeignKey("GuildId"); + + b.Navigation("Character"); + + b.Navigation("Guild"); + }); + + modelBuilder.Entity("Server.DB.Documents.InventoryItem", b => + { + b.HasOne("Server.DB.Documents.Character", "Character") + .WithMany("InventoryItems") + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Character"); + }); + + modelBuilder.Entity("Server.DB.Documents.Account", b => + { + b.Navigation("Characters"); + }); + + modelBuilder.Entity("Server.DB.Documents.Character", b => + { + b.Navigation("InventoryItems"); + }); + + modelBuilder.Entity("Server.DB.Documents.Guild", b => + { + b.Navigation("GuildMembers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Server/DB/Migrations/20231115183824_SwitchToDataAnnotations.cs b/Server/DB/Migrations/20231115183824_SwitchToDataAnnotations.cs new file mode 100644 index 0000000..7b9c842 --- /dev/null +++ b/Server/DB/Migrations/20231115183824_SwitchToDataAnnotations.cs @@ -0,0 +1,230 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Server.DB.Migrations; + +/// +public partial class SwitchToDataAnnotations : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Characters_Accounts_AccountId", + table: "Characters"); + + migrationBuilder.DropForeignKey( + name: "FK_Characters_Guild_GuildId", + table: "Characters"); + + migrationBuilder.DropForeignKey( + name: "FK_GuildMember_Characters_CharacterId", + table: "GuildMember"); + + migrationBuilder.DropForeignKey( + name: "FK_GuildMember_Guild_GuildId", + table: "GuildMember"); + + migrationBuilder.DropIndex( + name: "IX_Characters_Name", + table: "Characters"); + + migrationBuilder.DropColumn( + name: "ServerId", + table: "Characters"); + + migrationBuilder.AlterColumn( + name: "GuildId", + table: "GuildMember", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "CharacterId", + table: "GuildMember", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "Characters", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "varchar(20)", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "GuildId", + table: "Characters", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "AccountId", + table: "Characters", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.CreateIndex( + name: "IX_Accounts_Id", + table: "Accounts", + column: "Id", + unique: true); + + migrationBuilder.AddForeignKey( + name: "FK_Characters_Accounts_AccountId", + table: "Characters", + column: "AccountId", + principalTable: "Accounts", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_Characters_Guild_GuildId", + table: "Characters", + column: "GuildId", + principalTable: "Guild", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_GuildMember_Characters_CharacterId", + table: "GuildMember", + column: "CharacterId", + principalTable: "Characters", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_GuildMember_Guild_GuildId", + table: "GuildMember", + column: "GuildId", + principalTable: "Guild", + principalColumn: "Id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Characters_Accounts_AccountId", + table: "Characters"); + + migrationBuilder.DropForeignKey( + name: "FK_Characters_Guild_GuildId", + table: "Characters"); + + migrationBuilder.DropForeignKey( + name: "FK_GuildMember_Characters_CharacterId", + table: "GuildMember"); + + migrationBuilder.DropForeignKey( + name: "FK_GuildMember_Guild_GuildId", + table: "GuildMember"); + + migrationBuilder.DropIndex( + name: "IX_Accounts_Id", + table: "Accounts"); + + migrationBuilder.AlterColumn( + name: "GuildId", + table: "GuildMember", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000"), + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CharacterId", + table: "GuildMember", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000"), + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Name", + table: "Characters", + type: "varchar(20)", + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "GuildId", + table: "Characters", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000"), + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "AccountId", + table: "Characters", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000"), + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AddColumn( + name: "ServerId", + table: "Characters", + type: "smallint", + nullable: false, + defaultValue: (byte)0); + + migrationBuilder.CreateIndex( + name: "IX_Characters_Name", + table: "Characters", + column: "Name", + unique: true); + + migrationBuilder.AddForeignKey( + name: "FK_Characters_Accounts_AccountId", + table: "Characters", + column: "AccountId", + principalTable: "Accounts", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Characters_Guild_GuildId", + table: "Characters", + column: "GuildId", + principalTable: "Guild", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_GuildMember_Characters_CharacterId", + table: "GuildMember", + column: "CharacterId", + principalTable: "Characters", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_GuildMember_Guild_GuildId", + table: "GuildMember", + column: "GuildId", + principalTable: "Guild", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } +} diff --git a/Server/DB/Migrations/WonderkingContextModelSnapshot.cs b/Server/DB/Migrations/WonderkingContextModelSnapshot.cs index 5cff8b9..28aea68 100644 --- a/Server/DB/Migrations/WonderkingContextModelSnapshot.cs +++ b/Server/DB/Migrations/WonderkingContextModelSnapshot.cs @@ -45,6 +45,9 @@ namespace Server.DB.Migrations b.HasKey("Id"); + b.HasIndex("Id") + .IsUnique(); + b.HasIndex("Username") .IsUnique(); @@ -57,7 +60,7 @@ namespace Server.DB.Migrations .ValueGeneratedOnAdd() .HasColumnType("uuid"); - b.Property("AccountId") + b.Property("AccountId") .HasColumnType("uuid"); b.Property("Experience") @@ -66,7 +69,7 @@ namespace Server.DB.Migrations b.Property("Gender") .HasColumnType("smallint"); - b.Property("GuildId") + b.Property("GuildId") .HasColumnType("uuid"); b.Property("Health") @@ -88,23 +91,17 @@ namespace Server.DB.Migrations .HasColumnType("integer"); b.Property("Name") - .HasColumnType("varchar(20)"); + .HasColumnType("text"); b.Property("PvPLevel") .HasColumnType("smallint"); - b.Property("ServerId") - .HasColumnType("smallint"); - b.HasKey("Id"); b.HasIndex("AccountId"); b.HasIndex("GuildId"); - b.HasIndex("Name") - .IsUnique(); - b.ToTable("Characters"); }); @@ -131,10 +128,10 @@ namespace Server.DB.Migrations .ValueGeneratedOnAdd() .HasColumnType("uuid"); - b.Property("CharacterId") + b.Property("CharacterId") .HasColumnType("uuid"); - b.Property("GuildId") + b.Property("GuildId") .HasColumnType("uuid"); b.Property("Rank") @@ -170,12 +167,12 @@ namespace Server.DB.Migrations b.Property("Count") .HasColumnType("integer"); + b.Property("InventoryTab") + .HasColumnType("smallint"); + b.Property("ItemId") .HasColumnType("integer"); - b.Property("ItemType") - .HasColumnType("smallint"); - b.Property("Level") .HasColumnType("smallint"); @@ -205,15 +202,11 @@ namespace Server.DB.Migrations { b.HasOne("Server.DB.Documents.Account", "Account") .WithMany("Characters") - .HasForeignKey("AccountId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); + .HasForeignKey("AccountId"); b.HasOne("Server.DB.Documents.Guild", "Guild") .WithMany() - .HasForeignKey("GuildId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); + .HasForeignKey("GuildId"); b.OwnsOne("Wonderking.Packets.Outgoing.Data.BaseStats", "BaseStats", b1 => { @@ -284,15 +277,11 @@ namespace Server.DB.Migrations { b.HasOne("Server.DB.Documents.Character", "Character") .WithMany() - .HasForeignKey("CharacterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); + .HasForeignKey("CharacterId"); b.HasOne("Server.DB.Documents.Guild", "Guild") .WithMany("GuildMembers") - .HasForeignKey("GuildId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); + .HasForeignKey("GuildId"); b.Navigation("Character"); diff --git a/Server/DB/WonderkingContext.cs b/Server/DB/WonderkingContext.cs index af4e97e..8ec7cf6 100644 --- a/Server/DB/WonderkingContext.cs +++ b/Server/DB/WonderkingContext.cs @@ -24,30 +24,4 @@ public class WonderkingContext : DbContext .UseNpgsql( $"Host={this._configuration["DB:Host"]};Username={this._configuration["DB:Username"]};Password={this._configuration["DB:Password"]};Database={this._configuration["DB:Database"]};Port={this._configuration["DB:Port"]}") .EnableSensitiveDataLogging().UseLoggerFactory(this._loggerFactory); - - protected override void OnModelCreating(ModelBuilder modelBuilder) => - modelBuilder.Entity(builder => - { - builder.Property(b => b.Username).HasColumnType("varchar(20)"); - builder.HasIndex(b => b.Username).IsUnique(); - builder.Property(b => b.Password).HasColumnType("bytea"); - builder.Property(b => b.Salt).HasColumnType("bytea"); - builder.HasKey(b => b.Id); - builder.HasMany(e => e.Characters).WithOne(e => e.Account).HasForeignKey(e => e.AccountId) - .IsRequired(); - }).Entity(builder => - { - builder.HasKey(c => c.Id); - builder.Property(c => c.Name).HasColumnType("varchar(20)"); - builder.HasIndex(c => c.Name).IsUnique(); - builder.HasMany(e => e.InventoryItems).WithOne(e => e.Character) - .HasForeignKey(e => e.CharacterId).IsRequired(); - builder.OwnsOne(p => p.BaseStats); - builder.OwnsOne(p => p.JobData); - }).Entity(builder => { builder.HasKey(i => i.Id); }).Entity(builder => - { - builder.HasKey(g => g.Id); - builder.HasMany(g => g.GuildMembers).WithOne(g => g.Guild).HasForeignKey(g => g.GuildId) - .IsRequired(); - }); } diff --git a/Server/PacketHandlers/ChannelSelectionHandler.cs b/Server/PacketHandlers/ChannelSelectionHandler.cs index e67ca30..7e66231 100644 --- a/Server/PacketHandlers/ChannelSelectionHandler.cs +++ b/Server/PacketHandlers/ChannelSelectionHandler.cs @@ -46,7 +46,7 @@ public class ChannelSelectionHandler : IPacketHandler ChannelIsFullFlag = 0, Endpoint = "127.0.0.1", Port = 12345, - Characters = this._wonderkingContext.Characters.Where(c => c.AccountId == authSession.AccountId) + Characters = this._wonderkingContext.Characters.AsNoTracking().Where(c => c.Account.Id == authSession.AccountId) .Select(c => new CharacterData { @@ -59,11 +59,11 @@ public class ChannelSelectionHandler : IPacketHandler Health = c.Health, Mana = c.Mana, EquippedItems = - c.InventoryItems.Where(item => item.ItemType == ItemType.WornEquipment) + c.InventoryItems.Where(item => item.InventoryTab == InventoryTab.WornEquipment) .Select(item => item.ItemId) .ToArray(), EquippedCashItems = c.InventoryItems - .Where(item => item.ItemType == ItemType.WornCashEquipment) + .Where(item => item.InventoryTab == InventoryTab.WornCashEquipment) .Select(item => item.ItemId) .ToArray(), }) @@ -72,7 +72,7 @@ public class ChannelSelectionHandler : IPacketHandler guildNameResponsePacket = new CharacterSelectionSetGuildNamePacket { - GuildNames = this._wonderkingContext.Characters.Where(c => c.AccountId == authSession.AccountId) + GuildNames = this._wonderkingContext.Characters.Where(c => c.Account.Id == authSession.AccountId) .Select(character => character.Guild.Name).ToArray() }; } diff --git a/Server/PacketHandlers/CharacterCreationHandler..cs b/Server/PacketHandlers/CharacterCreationHandler..cs new file mode 100644 index 0000000..5ee5af6 --- /dev/null +++ b/Server/PacketHandlers/CharacterCreationHandler..cs @@ -0,0 +1,89 @@ +using Microsoft.EntityFrameworkCore; +using NetCoreServer; +using Server.DB; +using Server.DB.Documents; +using Server.Services; +using Wonderking.Game.Data.Character; +using Wonderking.Packets.Incoming; +using Wonderking.Packets.Outgoing; +using Wonderking.Packets.Outgoing.Data; + +namespace Server.PacketHandlers; + +public class CharacterCreationHandler : IPacketHandler +{ + private readonly WonderkingContext _wonderkingContext; + private readonly ItemObjectPoolService _itemObjectPoolService; + + public CharacterCreationHandler(WonderkingContext wonderkingContext, ItemObjectPoolService itemObjectPoolService) + { + _wonderkingContext = wonderkingContext; + _itemObjectPoolService = itemObjectPoolService; + } + + public Task HandleAsync(CharacterCreationPacket packet, TcpSession session) + { + var authSession = session as AuthSession; + var account = + _wonderkingContext.Accounts.FirstOrDefault(a => authSession != null && a.Id == authSession.AccountId); + var items = new List + { + _itemObjectPoolService.GetBaseInventoryItem(25), + _itemObjectPoolService.GetBaseInventoryItem(764), + _itemObjectPoolService.GetBaseInventoryItem(766), + _itemObjectPoolService.GetBaseInventoryItem(763), + _itemObjectPoolService.GetBaseInventoryItem(767) + }; + account?.Characters.Add(new Character + { + Account = account, + MapId = 300, + Name = packet.Name, + LastXCoordinate = 113, + LastYCoordinate = 0, + PvPLevel = PvPLevel.None, + Gender = Gender.None, + Experience = 0, + Level = 1, + InventoryItems = items, + BaseStats = new BaseStats + { + Strength = 5, + Dexterity = 5, + Intelligence = 5, + Vitality = 5, + Luck = 5, + Wisdom = 5 + }, + JobData = new JobData { FirstJob = packet.FirstJob, SecondJob = 0, ThirdJob = 0, FourthJob = 0 }, + Health = 250, + Mana = 250, + }); + _wonderkingContext.SaveChanges(); + + var character = this._wonderkingContext.Characters.AsNoTracking() + .Where(c => authSession != null && c.Account.Id == authSession.AccountId && c.Name == packet.Name) + .Select(c => + new CharacterData + { + Name = c.Name, + Job = c.JobData, + Gender = c.Gender, + Level = c.Level, + Experience = 0, + Stats = c.BaseStats, + Health = c.Health, + Mana = c.Mana, + EquippedItems = + c.InventoryItems.Where(item => item.InventoryTab == InventoryTab.WornEquipment) + .Select(item => item.ItemId) + .ToArray(), + EquippedCashItems = c.InventoryItems + .Where(item => item.InventoryTab == InventoryTab.WornCashEquipment) + .Select(item => item.ItemId) + .ToArray(), + }).FirstOrDefault(); + authSession?.Send(new CharacterCreationResponsePacket { Character = character }); + return Task.CompletedTask; + } +} diff --git a/Server/PacketHandlers/CharacterNameCheckHandler.cs b/Server/PacketHandlers/CharacterNameCheckHandler.cs new file mode 100644 index 0000000..c97af26 --- /dev/null +++ b/Server/PacketHandlers/CharacterNameCheckHandler.cs @@ -0,0 +1,25 @@ +using NetCoreServer; +using Server.DB; +using Wonderking.Packets.Incoming; +using Wonderking.Packets.Outgoing; + +namespace Server.PacketHandlers; + +public class CharacterNameCheckHandler : IPacketHandler +{ + private readonly WonderkingContext _wonderkingContext; + + public CharacterNameCheckHandler(WonderkingContext wonderkingContext) + { + _wonderkingContext = wonderkingContext; + } + + public Task HandleAsync(CharacterNameCheckPacket packet, TcpSession session) + { + var isTaken = _wonderkingContext.Characters.Any(c => c.Name == packet.Name); + var responsePacket = new CharacterNameCheckPacketResponse { IsTaken = isTaken }; + var authSession = session as AuthSession; + authSession?.Send(responsePacket); + return Task.CompletedTask; + } +} diff --git a/Server/Program.cs b/Server/Program.cs index 70ff82c..a092fe9 100644 --- a/Server/Program.cs +++ b/Server/Program.cs @@ -22,9 +22,10 @@ builder.Logging.AddFile("Logs/Server-{Date}.json.log", LogLevel.Trace, isJson: t builder.Services.AddEntityFrameworkNpgsql(); builder.Services.AddDbContext(); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddHostedService(); builder.Services.AddHostedService(provider => provider.GetService() ?? throw new InvalidOperationException()); -builder.Services.AddSingleton(); builder.Services.AddMassTransit(x => { x.UsingInMemory((context, configurator) => configurator.ConfigureEndpoints(context)); diff --git a/Server/Services/ItemObjectPoolService.cs b/Server/Services/ItemObjectPoolService.cs index 3a8ac45..2bce29b 100644 --- a/Server/Services/ItemObjectPoolService.cs +++ b/Server/Services/ItemObjectPoolService.cs @@ -1,6 +1,8 @@ using System.Collections.Concurrent; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Server.DB.Documents; using Wonderking.Game.Data; using Wonderking.Game.Reader; @@ -10,9 +12,11 @@ public class ItemObjectPoolService : IHostedService { readonly ConcurrentDictionary _itemObjectPool = new(); private readonly ItemReader _itemReader; + private readonly ILogger _logger; - public ItemObjectPoolService(IConfiguration configuration) + public ItemObjectPoolService(IConfiguration configuration, ILogger logger) { + _logger = logger; _itemReader = new ItemReader(configuration.GetSection("Game").GetSection("Data").GetValue("Path") ?? string.Empty); } @@ -20,23 +24,31 @@ public class ItemObjectPoolService : IHostedService public Task StartAsync(CancellationToken cancellationToken) { var amountOfEntries = _itemReader.GetAmountOfEntries(); - ParallelEnumerable.Range(0, (int)amountOfEntries).AsParallel().ForAll(i => + Parallel.For(0, (int)amountOfEntries, i => { var itemObject = _itemReader.GetEntry((uint)i); - _itemObjectPool.TryAdd(itemObject.ItemID, itemObject); + var result = _itemObjectPool.TryAdd(itemObject.ItemID, itemObject); + if (!result) + { + throw new Exception($"Failed to add item {itemObject.ItemID} to the item object pool"); + } + + _logger.LogTrace("Item with {ID} has been added", itemObject.ItemID); }); + _logger.LogInformation("A total of {AmountOfEntries} items have been added to the item object pool", + _itemObjectPool.Count); return Task.CompletedTask; } public Task StopAsync(CancellationToken cancellationToken) { - _itemReader.Dispose(); return Task.CompletedTask; } public ItemObject GetItem(ushort itemId) { - return _itemObjectPool[itemId]; + _ = _itemObjectPool.TryGetValue(itemId, out var itemObject); + return itemObject; } public bool ContainsItem(ushort itemId) @@ -48,4 +60,24 @@ public class ItemObjectPoolService : IHostedService { return _itemObjectPool.AsReadOnly().Values.AsQueryable(); } + + public InventoryItem GetBaseInventoryItem(ushort itemId, ushort count = 1, bool isWorn = false) + { + var item = this.GetItem(itemId); + return new InventoryItem + { + ItemId = itemId, + Count = count, + Slot = 0, + InventoryTab = InventoryTab.WornEquipment, + Level = item.MinimumLevelRequirement, + Rarity = 0, + AddOption = 0, + AddOption2 = 0, + AddOption3 = 0, + Option = 0, + Option2 = 0, + Option3 = 0 + }; + } } diff --git a/Server/docker-compose.yml b/Server/docker-compose.yml index bd3cc51..657f148 100644 --- a/Server/docker-compose.yml +++ b/Server/docker-compose.yml @@ -12,14 +12,14 @@ - DB:Port=5432 - DB:Username=continuity - DB:Password=continuity - - Game:Data:Path=/app/data + - Game:Data:Path=/app/data/ networks: - continuity ports: - "10001:10001" volumes: - type: bind - source: game-data + source: ../wk-data target: /app/data read_only: true diff --git a/Server/settings.Development.json b/Server/settings.Development.json index ded1040..3d15198 100644 --- a/Server/settings.Development.json +++ b/Server/settings.Development.json @@ -11,7 +11,7 @@ }, "Game":{ "Data":{ - "Path": "../wk-data" + "Path": "../wk-data/" } } } diff --git a/Wonderking/Game/DataReader.cs b/Wonderking/Game/DataReader.cs index e67c8a4..6563c85 100644 --- a/Wonderking/Game/DataReader.cs +++ b/Wonderking/Game/DataReader.cs @@ -7,7 +7,7 @@ public abstract class DataReader protected DataReader(string path) { Path = path; - DatFileContent = new(GetDatFileContent(path).ToArray()); + DatFileContent = GetDatFileContent(path).ToArray(); } private protected string Path { get; init; } @@ -33,7 +33,7 @@ public abstract class DataReader throw new NotSupportedException("XorKey is null"); } - protected MemoryStream DatFileContent { get; } + protected byte[] DatFileContent { get; } private static Span GetDatFileContent(string path) { diff --git a/Wonderking/Game/Reader/ItemReader.cs b/Wonderking/Game/Reader/ItemReader.cs index 38ae8a3..8b00ff0 100644 --- a/Wonderking/Game/Reader/ItemReader.cs +++ b/Wonderking/Game/Reader/ItemReader.cs @@ -1,9 +1,8 @@ using Wonderking.Game.Data; -using Wonderking.Game.Data.Item; namespace Wonderking.Game.Reader; -public class ItemReader(string path) : DataReader(path), IDisposable +public class ItemReader(string path) : DataReader(path) { public override uint GetAmountOfEntries() { @@ -13,8 +12,9 @@ public class ItemReader(string path) : DataReader(path), IDisposable public override ItemObject GetEntry(uint entryId) { var item = new ItemObject(); - this.DatFileContent.Position = 9 + entryId * this.GetSizeOfEntry(); - var reader = new BinaryReader(this.DatFileContent); + var arraySegment = new ArraySegment(DatFileContent, + 9 + (int)entryId * this.GetSizeOfEntry(), this.GetSizeOfEntry()); + var reader = new BinaryReader(new MemoryStream(arraySegment.ToArray())); item.ItemID = reader.ReadUInt32(); //9 item.Disabled = reader.ReadUInt32() == 1; //13 item.ItemType = reader.ReadUInt32(); //17 @@ -100,8 +100,4 @@ public class ItemReader(string path) : DataReader(path), IDisposable return item; } - public void Dispose() - { - this.DatFileContent.Dispose(); - } } diff --git a/Wonderking/Packets/Incoming/CharacterCreationPacket.cs b/Wonderking/Packets/Incoming/CharacterCreationPacket.cs new file mode 100644 index 0000000..105f6db --- /dev/null +++ b/Wonderking/Packets/Incoming/CharacterCreationPacket.cs @@ -0,0 +1,39 @@ +using System.Text; +using Wonderking.Game.Data.Character; + +namespace Wonderking.Packets.Incoming; + +[PacketId(OperationCode.CharacterCreation)] +public class CharacterCreationPacket : IPacket +{ + public byte Slot { get; set; } + public byte Unknown { get; set; } + public ushort Id { get; set; } + + public string Name { get; set; } + public byte FirstJob { get; set; } + public Gender Gender { get; set; } + public byte Hair { get; set; } + public byte Eyes { get; set; } + public byte Shirt { get; set; } + public byte Pants { get; set; } + + public void Deserialize(byte[] data) + { + Slot = data[0]; + Unknown = data[1]; + Id = BitConverter.ToUInt16(data, 2); + Name = Encoding.ASCII.GetString(data, 4, 20).TrimEnd('\0').TrimEnd('\n').TrimEnd('\0'); + FirstJob = data[24]; + Gender = (Gender)data[25]; + Hair = data[26]; + Eyes = data[27]; + Shirt = data[28]; + Pants = data[29]; + } + + public byte[] Serialize() + { + throw new NotSupportedException(); + } +} diff --git a/Wonderking/Packets/Incoming/CharacterNameCheckPacket.cs b/Wonderking/Packets/Incoming/CharacterNameCheckPacket.cs new file mode 100644 index 0000000..6799961 --- /dev/null +++ b/Wonderking/Packets/Incoming/CharacterNameCheckPacket.cs @@ -0,0 +1,19 @@ +using System.Text; + +namespace Wonderking.Packets.Incoming; + +[PacketId(OperationCode.CharacterNameCheck)] +public class CharacterNameCheckPacket : IPacket +{ + public required string Name { get; set; } + + public void Deserialize(byte[] data) + { + Encoding.ASCII.GetString(data, 0, 20); + } + + public byte[] Serialize() + { + throw new NotSupportedException(); + } +} diff --git a/Wonderking/Packets/OperationCode.cs b/Wonderking/Packets/OperationCode.cs index 97e5584..28967dd 100644 --- a/Wonderking/Packets/OperationCode.cs +++ b/Wonderking/Packets/OperationCode.cs @@ -6,5 +6,11 @@ public enum OperationCode : ushort LoginResponse = 12, ChannelSelection = 13, ChannelSelectionResponse = 13, + CharacterNameCheck = 14, + CharacterNameCheckResponse = 14, + CharacterCreation = 15, + CharacterCreationResponse = 15, + CharacterDeletion = 16, + CharacterSelection = 17, CharacterSelectionSetGuildName = 19, } diff --git a/Wonderking/Packets/Outgoing/CharacterCreationResponsePacket.cs b/Wonderking/Packets/Outgoing/CharacterCreationResponsePacket.cs new file mode 100644 index 0000000..c38e7a5 --- /dev/null +++ b/Wonderking/Packets/Outgoing/CharacterCreationResponsePacket.cs @@ -0,0 +1,58 @@ +using System.Buffers.Binary; +using System.Text; +using Wonderking.Packets.Outgoing.Data; + +namespace Wonderking.Packets.Outgoing; + +[PacketId(OperationCode.CharacterCreationResponse)] +public class CharacterCreationResponsePacket : IPacket +{ + public required CharacterData Character { get; set; } + + public void Deserialize(byte[] data) + { + throw new NotSupportedException(); + } + + public byte[] Serialize() + { + Span data = stackalloc byte[132]; + + BinaryPrimitives.WriteInt32LittleEndian(data.Slice(0 + 132, 4), 0); + Encoding.ASCII.GetBytes(Character.Name, data.Slice(4 + 132, 20)); + + // Job Data + data[24 + 132] = Character.Job.FirstJob; + data[25 + 132] = Character.Job.SecondJob; + data[26 + 132] = Character.Job.ThirdJob; + data[27 + 132] = Character.Job.FourthJob; + + data[28 + 132] = (byte)Character.Gender; + BinaryPrimitives.WriteUInt16LittleEndian(data.Slice(49 + 132, 2), Character.Level); + data[31 + 132] = (byte)Character.Experience; + + // Stats + BinaryPrimitives.WriteInt16LittleEndian(data.Slice(32 + 132, 2), Character.Stats.Strength); + BinaryPrimitives.WriteInt16LittleEndian(data.Slice(34 + 132, 2), Character.Stats.Dexterity); + BinaryPrimitives.WriteInt16LittleEndian(data.Slice(36 + 132, 2), Character.Stats.Intelligence); + BinaryPrimitives.WriteInt16LittleEndian(data.Slice(38 + 132, 2), Character.Stats.Vitality); + BinaryPrimitives.WriteInt16LittleEndian(data.Slice(40 + 132, 2), Character.Stats.Luck); + BinaryPrimitives.WriteInt16LittleEndian(data.Slice(42 + 132, 2), Character.Stats.Wisdom); + + BinaryPrimitives.WriteInt32LittleEndian(data.Slice(44 + 132, 4), Character.Health); + BinaryPrimitives.WriteInt32LittleEndian(data.Slice(48 + 132, 4), Character.Mana); + + for (var i = 0; i < 20; i++) + { + // Equipped Items + BinaryPrimitives.WriteUInt16LittleEndian(data.Slice(52 + 132 + i * 2, 2), + Character.EquippedItems.Length > i ? Character.EquippedItems[i] : (ushort)0); + + // Equipped Cash Items + BinaryPrimitives.WriteUInt16LittleEndian(data.Slice(92 + 132 + i * 2, 2), + Character.EquippedCashItems.Length > i ? Character.EquippedCashItems[i] : (ushort)0); + } + + return data.ToArray(); + } +} diff --git a/Wonderking/Packets/Outgoing/CharacterNameCheckPacketResponse.cs b/Wonderking/Packets/Outgoing/CharacterNameCheckPacketResponse.cs new file mode 100644 index 0000000..ac9e6a0 --- /dev/null +++ b/Wonderking/Packets/Outgoing/CharacterNameCheckPacketResponse.cs @@ -0,0 +1,19 @@ +namespace Wonderking.Packets.Outgoing; + +[PacketId(OperationCode.CharacterNameCheckResponse)] +public class CharacterNameCheckPacketResponse : IPacket +{ + public required bool IsTaken { get; set; } + + public void Deserialize(byte[] data) + { + throw new NotSupportedException(); + } + + public byte[] Serialize() + { + Span data = stackalloc byte[1]; + data[0] = this.IsTaken ? (byte)1 : (byte)0; + return data.ToArray(); + } +} diff --git a/Wonderking/Packets/Outgoing/Data/BaseStats.cs b/Wonderking/Packets/Outgoing/Data/BaseStats.cs index cb5cbbe..1dfba40 100644 --- a/Wonderking/Packets/Outgoing/Data/BaseStats.cs +++ b/Wonderking/Packets/Outgoing/Data/BaseStats.cs @@ -1,8 +1,10 @@ using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore; namespace Wonderking.Packets.Outgoing.Data; [UsedImplicitly] +[Owned] public class BaseStats { public required short Strength { get; set; } diff --git a/Wonderking/Packets/Outgoing/Data/JobData.cs b/Wonderking/Packets/Outgoing/Data/JobData.cs index fb9578a..bb385cc 100644 --- a/Wonderking/Packets/Outgoing/Data/JobData.cs +++ b/Wonderking/Packets/Outgoing/Data/JobData.cs @@ -1,8 +1,10 @@ using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore; namespace Wonderking.Packets.Outgoing.Data; [UsedImplicitly] +[Owned] public class JobData { public required byte FirstJob { get; set; } diff --git a/Wonderking/Wonderking.csproj b/Wonderking/Wonderking.csproj index f1e377f..97a08ea 100644 --- a/Wonderking/Wonderking.csproj +++ b/Wonderking/Wonderking.csproj @@ -22,6 +22,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + all runtime; build; native; contentfiles; analyzers; buildtransitive