diff --git a/.editorconfig b/.editorconfig
index e270271..6389e64 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -28,6 +28,8 @@ insert_final_newline = true
indent_size = 4
dotnet_sort_system_directives_first = true
+MA0004.report = DetectContext # (default) Try to detect the current context and report only if it considers ConfigureAwait is needed
+MA0004.report = Always # Always report missing ConfigureAwait whatever the context
# Don't use this. qualifier
dotnet_style_qualification_for_field = false:suggestion
dotnet_style_qualification_for_property = false:suggestion
diff --git a/.gitignore b/.gitignore
index d0af085..4789088 100644
--- a/.gitignore
+++ b/.gitignore
@@ -482,3 +482,5 @@ $RECYCLE.BIN/
.vscode
.nuke
+
+wk-data
diff --git a/Benchmarks/Benchmarks.csproj b/Benchmarks/Benchmarks.csproj
index 8b71371..eb55e9f 100644
--- a/Benchmarks/Benchmarks.csproj
+++ b/Benchmarks/Benchmarks.csproj
@@ -4,13 +4,21 @@
Exe
enable
enable
- preview
- net6.0;net7.0
+ 12
+ net6.0;net7.0;net8.0
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
-
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/Benchmarks/BinaryConversionBenchmarks.cs b/Benchmarks/BinaryConversionBenchmarks.cs
index 74cef4c..ff14eee 100644
--- a/Benchmarks/BinaryConversionBenchmarks.cs
+++ b/Benchmarks/BinaryConversionBenchmarks.cs
@@ -2,30 +2,26 @@ namespace Benchmarks;
using System.Security.Cryptography;
using BenchmarkDotNet.Attributes;
-using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Order;
-[SimpleJob(RuntimeMoniker.Net80)]
-[SimpleJob(RuntimeMoniker.Net70)]
-[SimpleJob(RuntimeMoniker.Net60)]
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
-[RankColumn]
-[MemoryDiagnoser]
-[ThreadingDiagnoser]
+[Config(typeof(GenericConfig))]
public class BinaryConversionBenchmarks
{
private byte[] _data = null!;
private int _offset;
+ private int _writeBuffer;
[GlobalSetup]
public void Setup()
{
this._data = RandomNumberGenerator.GetBytes(4000);
this._offset = RandomNumberGenerator.GetInt32(0, 3500);
+ this._writeBuffer = RandomNumberGenerator.GetInt32(int.MinValue, int.MaxValue);
}
[Benchmark]
- public short BitConverterTest() => BitConverter.ToInt16(this._data, this._offset);
+ public short BitConverterParseTest() => BitConverter.ToInt16(this._data, this._offset);
[Benchmark]
public short BinaryReader()
@@ -37,7 +33,30 @@ public class BinaryConversionBenchmarks
}
[Benchmark]
- public short BinaryPrimitives() =>
+ public short BinaryPrimitivesRead() =>
System.Buffers.Binary.BinaryPrimitives.ReadInt16LittleEndian(
new ArraySegment(this._data, this._offset, sizeof(short)));
+
+ [Benchmark]
+ public void BinaryPrimitivesWrite()
+ {
+ System.Buffers.Binary.BinaryPrimitives.WriteInt32LittleEndian(this._data.AsSpan(_offset, 4),
+ this._writeBuffer);
+ }
+
+ [Benchmark]
+ public void BitConverterCopy()
+ {
+ BitConverter.GetBytes(this._writeBuffer).CopyTo(this._data, this._offset);
+ }
+
+ [Benchmark]
+ public void BitConverterAssignment()
+ {
+ var bytes = BitConverter.GetBytes(this._writeBuffer);
+ this._data[this._offset] = bytes[0];
+ this._data[this._offset + 1] = bytes[1];
+ this._data[this._offset + 2] = bytes[2];
+ this._data[this._offset + 3] = bytes[3];
+ }
}
diff --git a/Benchmarks/DataCacheBenchmark.cs b/Benchmarks/DataCacheBenchmark.cs
new file mode 100644
index 0000000..27e78b4
--- /dev/null
+++ b/Benchmarks/DataCacheBenchmark.cs
@@ -0,0 +1,85 @@
+using System.Collections.Concurrent;
+using System.Collections.Immutable;
+
+namespace Benchmarks;
+
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Order;
+
+[Config(typeof(GenericConfig))]
+[Orderer(SummaryOrderPolicy.FastestToSlowest)]
+public class DataCacheBenchmark
+{
+ [Params(1000, 100000, 1000000)] public int N;
+ private HashSet _hashSet;
+ private Dictionary _dictionary;
+ private ConcurrentDictionary _concurrentDictionary;
+ private ImmutableHashSet _immutableHashSet;
+
+ [GlobalSetup]
+ public void Setup()
+ {
+ _hashSet = new HashSet();
+ _dictionary = new Dictionary();
+ _concurrentDictionary = new ConcurrentDictionary();
+ _immutableHashSet = ImmutableHashSet.Empty;
+
+ _hashSet.Clear();
+ _dictionary.Clear();
+ _concurrentDictionary.Clear();
+ _immutableHashSet = _immutableHashSet.Clear();
+ _hashSet.EnsureCapacity(N);
+ _dictionary.EnsureCapacity(N);
+
+ for (var i = 0; i < N; i++)
+ {
+ _immutableHashSet = _immutableHashSet.Add(i);
+ _hashSet.Add(i);
+ _dictionary.Add(i, i);
+ _concurrentDictionary.TryAdd(i, i);
+ }
+ }
+
+ [Benchmark]
+ public void HashSetAdd()
+ {
+ ParallelEnumerable.Range(0, N).AsParallel().ForAll(i => _hashSet.Add(i));
+ }
+
+ [Benchmark]
+ public void DictionaryAdd()
+ {
+ ParallelEnumerable.Range(0, N).AsParallel().ForAll(i => _dictionary.Add(N + i, i));
+ }
+
+ [Benchmark]
+ public void ConcurrentDictionaryAddOrUpdate()
+ {
+ ParallelEnumerable.Range(0, N).AsParallel().ForAll(i =>
+ _concurrentDictionary.AddOrUpdate(N + i, i, (key, oldValue) => oldValue + i));
+ }
+
+ [Benchmark]
+ public void ImmutableHashSetLookup()
+ {
+ ParallelEnumerable.Range(0, N).AsParallel().ForAll(i => _immutableHashSet.Contains(i));
+ }
+
+ [Benchmark]
+ public void HashSetLookup()
+ {
+ ParallelEnumerable.Range(0, N).AsParallel().ForAll(i => _hashSet.Contains(i));
+ }
+
+ [Benchmark]
+ public void DictionaryLookup()
+ {
+ ParallelEnumerable.Range(0, N).AsParallel().ForAll(i => _dictionary.ContainsKey(i));
+ }
+
+ [Benchmark]
+ public void ConcurrentDictionaryLookup()
+ {
+ ParallelEnumerable.Range(0, N).AsParallel().ForAll(i => _concurrentDictionary.ContainsKey(i));
+ }
+}
diff --git a/Benchmarks/GenericConfig.cs b/Benchmarks/GenericConfig.cs
new file mode 100644
index 0000000..903dbf4
--- /dev/null
+++ b/Benchmarks/GenericConfig.cs
@@ -0,0 +1,31 @@
+using BenchmarkDotNet.Analysers;
+using BenchmarkDotNet.Columns;
+using BenchmarkDotNet.Configs;
+using BenchmarkDotNet.Diagnosers;
+using BenchmarkDotNet.Environments;
+using BenchmarkDotNet.Exporters;
+using BenchmarkDotNet.Exporters.Csv;
+using BenchmarkDotNet.Jobs;
+
+namespace Benchmarks;
+
+public class GenericConfig : ManualConfig
+{
+ public GenericConfig()
+ {
+ AddJob(Job.Default
+ .WithRuntime(CoreRuntime.Core80))
+ .AddDiagnoser(ThreadingDiagnoser.Default, MemoryDiagnoser.Default,
+ EventPipeProfiler.Default)
+ .AddAnalyser(MinIterationTimeAnalyser.Default, OutliersAnalyser.Default,
+ RuntimeErrorAnalyser.Default, EnvironmentAnalyser.Default)
+ .AddColumn(RankColumn.Arabic).AddExporter(CsvExporter.Default, MarkdownExporter.Default);
+ AddJob(Job.Default
+ .WithRuntime(CoreRuntime.Core70))
+ .AddDiagnoser(ThreadingDiagnoser.Default, MemoryDiagnoser.Default,
+ EventPipeProfiler.Default)
+ .AddAnalyser(MinIterationTimeAnalyser.Default, OutliersAnalyser.Default,
+ RuntimeErrorAnalyser.Default, EnvironmentAnalyser.Default)
+ .AddColumn(RankColumn.Arabic).AddExporter(CsvExporter.Default, MarkdownExporter.Default);
+ }
+}
diff --git a/Server/DB/Documents/Character.cs b/Server/DB/Documents/Character.cs
index ee9be2c..ad9c06e 100644
--- a/Server/DB/Documents/Character.cs
+++ b/Server/DB/Documents/Character.cs
@@ -1,3 +1,6 @@
+using Wonderking.Game.Data.Character;
+using Wonderking.Packets.Outgoing;
+
namespace Server.DB.Documents;
public class Character
@@ -14,4 +17,13 @@ public class Character
public Gender Gender { get; set; }
public long Experience { get; set; }
public byte Level { get; set; }
+ public 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; }
}
diff --git a/Server/DB/Documents/Guild.cs b/Server/DB/Documents/Guild.cs
new file mode 100644
index 0000000..a907566
--- /dev/null
+++ b/Server/DB/Documents/Guild.cs
@@ -0,0 +1,9 @@
+namespace Server.DB.Documents;
+
+public class Guild
+{
+ public Guid Id { get; set; }
+ public string Name { get; set; }
+ public string Notice { get; set; }
+ public ICollection GuildMembers { get; set; }
+}
diff --git a/Server/DB/Documents/GuildMember.cs b/Server/DB/Documents/GuildMember.cs
new file mode 100644
index 0000000..44cb91b
--- /dev/null
+++ b/Server/DB/Documents/GuildMember.cs
@@ -0,0 +1,11 @@
+namespace Server.DB.Documents;
+
+public class GuildMember
+{
+ 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/GuildRank.cs b/Server/DB/Documents/GuildRank.cs
new file mode 100644
index 0000000..bf36540
--- /dev/null
+++ b/Server/DB/Documents/GuildRank.cs
@@ -0,0 +1,11 @@
+namespace Server.DB.Documents;
+
+public enum GuildRank : byte
+{
+ Initiate = 0,
+ Member = 1,
+ Veteran = 2,
+ Elite = 3,
+ Officer = 4,
+ Master = 5
+}
diff --git a/Server/DB/Documents/InventoryItem.cs b/Server/DB/Documents/InventoryItem.cs
new file mode 100644
index 0000000..58ddd55
--- /dev/null
+++ b/Server/DB/Documents/InventoryItem.cs
@@ -0,0 +1,20 @@
+namespace Server.DB.Documents;
+
+public class InventoryItem
+{
+ public Guid CharacterId { get; set; }
+ public Character Character { get; set; }
+ public Guid Id { get; set; }
+ public ushort ItemId { get; set; }
+ public ushort Count { get; set; }
+ public byte Slot { get; set; }
+ public ItemType ItemType { get; set; }
+ public byte Level { get; set; }
+ public byte Rarity { get; set; }
+ public byte AddOption { get; set; }
+ public byte AddOption2 { get; set; }
+ public byte AddOption3 { get; set; }
+ public short Option { get; set; }
+ public short Option2 { get; set; }
+ public short Option3 { get; set; }
+}
diff --git a/Server/DB/Documents/ItemType.cs b/Server/DB/Documents/ItemType.cs
new file mode 100644
index 0000000..2b12c16
--- /dev/null
+++ b/Server/DB/Documents/ItemType.cs
@@ -0,0 +1,12 @@
+namespace Server.DB.Documents;
+
+public enum ItemType : byte
+{
+ WornEquipment = 0,
+ WornCashEquipment = 1,
+ Equipment = 2,
+ Etc = 3,
+ Cash = 4,
+ Warehouse = 5,
+ GiftBox = 6
+}
diff --git a/Server/DB/Migrations/20231108143729_AddInventoryToCharacter.Designer.cs b/Server/DB/Migrations/20231108143729_AddInventoryToCharacter.Designer.cs
new file mode 100644
index 0000000..86952dc
--- /dev/null
+++ b/Server/DB/Migrations/20231108143729_AddInventoryToCharacter.Designer.cs
@@ -0,0 +1,183 @@
+//
+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("20231108143729_AddInventoryToCharacter")]
+ partial class AddInventoryToCharacter
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "7.0.13")
+ .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.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("LastXCoordinate")
+ .HasColumnType("smallint");
+
+ b.Property("LastYCoordinate")
+ .HasColumnType("smallint");
+
+ b.Property("Level")
+ .HasColumnType("smallint");
+
+ 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.ToTable("Characters");
+ });
+
+ 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("ItemId")
+ .HasColumnType("smallint");
+
+ b.Property("ItemType")
+ .HasColumnType("smallint");
+
+ 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.Navigation("Account");
+ });
+
+ 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");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Server/DB/Migrations/20231108143729_AddInventoryToCharacter.cs b/Server/DB/Migrations/20231108143729_AddInventoryToCharacter.cs
new file mode 100644
index 0000000..4e365ba
--- /dev/null
+++ b/Server/DB/Migrations/20231108143729_AddInventoryToCharacter.cs
@@ -0,0 +1,55 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Server.DB.Migrations;
+
+///
+public partial class AddInventoryToCharacter : Migration
+{
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "InventoryItem",
+ columns: table => new
+ {
+ Id = table.Column(type: "uuid", nullable: false),
+ CharacterId = table.Column(type: "uuid", nullable: false),
+ ItemId = table.Column(type: "smallint", nullable: false),
+ Count = table.Column(type: "integer", nullable: false),
+ Slot = table.Column(type: "smallint", nullable: false),
+ ItemType = table.Column(type: "smallint", nullable: false),
+ Level = table.Column(type: "smallint", nullable: false),
+ Rarity = table.Column(type: "smallint", nullable: false),
+ AddOption = table.Column(type: "smallint", nullable: false),
+ AddOption2 = table.Column(type: "smallint", nullable: false),
+ AddOption3 = table.Column(type: "smallint", nullable: false),
+ Option = table.Column(type: "smallint", nullable: false),
+ Option2 = table.Column(type: "smallint", nullable: false),
+ Option3 = table.Column(type: "smallint", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_InventoryItem", x => x.Id);
+ table.ForeignKey(
+ name: "FK_InventoryItem_Characters_CharacterId",
+ column: x => x.CharacterId,
+ principalTable: "Characters",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_InventoryItem_CharacterId",
+ table: "InventoryItem",
+ column: "CharacterId");
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "InventoryItem");
+ }
+}
diff --git a/Server/DB/Migrations/20231113192405_AdditionalCharacterData.Designer.cs b/Server/DB/Migrations/20231113192405_AdditionalCharacterData.Designer.cs
new file mode 100644
index 0000000..345b7b3
--- /dev/null
+++ b/Server/DB/Migrations/20231113192405_AdditionalCharacterData.Designer.cs
@@ -0,0 +1,249 @@
+//
+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("20231113192405_AdditionalCharacterData")]
+ partial class AdditionalCharacterData
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "7.0.13")
+ .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.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("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.ToTable("Characters");
+ });
+
+ 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("ItemId")
+ .HasColumnType("integer");
+
+ b.Property("ItemType")
+ .HasColumnType("smallint");
+
+ 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.OwnsOne("Wonderking.Packets.Outgoing.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.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("JobData");
+ });
+
+ 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");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Server/DB/Migrations/20231113192405_AdditionalCharacterData.cs b/Server/DB/Migrations/20231113192405_AdditionalCharacterData.cs
new file mode 100644
index 0000000..49c9278
--- /dev/null
+++ b/Server/DB/Migrations/20231113192405_AdditionalCharacterData.cs
@@ -0,0 +1,155 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Server.DB.Migrations;
+
+///
+public partial class AdditionalCharacterData : Migration
+{
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AlterColumn(
+ name: "ItemId",
+ table: "InventoryItem",
+ type: "integer",
+ nullable: false,
+ oldClrType: typeof(short),
+ oldType: "smallint");
+
+ migrationBuilder.AddColumn(
+ name: "BaseStats_Dexterity",
+ table: "Characters",
+ type: "smallint",
+ nullable: true);
+
+ migrationBuilder.AddColumn(
+ name: "BaseStats_Intelligence",
+ table: "Characters",
+ type: "smallint",
+ nullable: true);
+
+ migrationBuilder.AddColumn(
+ name: "BaseStats_Luck",
+ table: "Characters",
+ type: "smallint",
+ nullable: true);
+
+ migrationBuilder.AddColumn(
+ name: "BaseStats_Strength",
+ table: "Characters",
+ type: "smallint",
+ nullable: true);
+
+ migrationBuilder.AddColumn(
+ name: "BaseStats_Vitality",
+ table: "Characters",
+ type: "smallint",
+ nullable: true);
+
+ migrationBuilder.AddColumn(
+ name: "BaseStats_Wisdom",
+ table: "Characters",
+ type: "smallint",
+ nullable: true);
+
+ migrationBuilder.AddColumn(
+ name: "Health",
+ table: "Characters",
+ type: "integer",
+ nullable: false,
+ defaultValue: 0);
+
+ migrationBuilder.AddColumn(
+ name: "JobData_FirstJob",
+ table: "Characters",
+ type: "smallint",
+ nullable: true);
+
+ migrationBuilder.AddColumn(
+ name: "JobData_FourthJob",
+ table: "Characters",
+ type: "smallint",
+ nullable: true);
+
+ migrationBuilder.AddColumn(
+ name: "JobData_SecondJob",
+ table: "Characters",
+ type: "smallint",
+ nullable: true);
+
+ migrationBuilder.AddColumn(
+ name: "JobData_ThirdJob",
+ table: "Characters",
+ type: "smallint",
+ nullable: true);
+
+ migrationBuilder.AddColumn(
+ name: "Mana",
+ table: "Characters",
+ type: "integer",
+ nullable: false,
+ defaultValue: 0);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "BaseStats_Dexterity",
+ table: "Characters");
+
+ migrationBuilder.DropColumn(
+ name: "BaseStats_Intelligence",
+ table: "Characters");
+
+ migrationBuilder.DropColumn(
+ name: "BaseStats_Luck",
+ table: "Characters");
+
+ migrationBuilder.DropColumn(
+ name: "BaseStats_Strength",
+ table: "Characters");
+
+ migrationBuilder.DropColumn(
+ name: "BaseStats_Vitality",
+ table: "Characters");
+
+ migrationBuilder.DropColumn(
+ name: "BaseStats_Wisdom",
+ table: "Characters");
+
+ migrationBuilder.DropColumn(
+ name: "Health",
+ table: "Characters");
+
+ migrationBuilder.DropColumn(
+ name: "JobData_FirstJob",
+ table: "Characters");
+
+ migrationBuilder.DropColumn(
+ name: "JobData_FourthJob",
+ table: "Characters");
+
+ migrationBuilder.DropColumn(
+ name: "JobData_SecondJob",
+ table: "Characters");
+
+ migrationBuilder.DropColumn(
+ name: "JobData_ThirdJob",
+ table: "Characters");
+
+ migrationBuilder.DropColumn(
+ name: "Mana",
+ table: "Characters");
+
+ migrationBuilder.AlterColumn(
+ name: "ItemId",
+ table: "InventoryItem",
+ type: "smallint",
+ nullable: false,
+ oldClrType: typeof(int),
+ oldType: "integer");
+ }
+}
diff --git a/Server/DB/Migrations/20231114184404_AddGuildData.Designer.cs b/Server/DB/Migrations/20231114184404_AddGuildData.Designer.cs
new file mode 100644
index 0000000..3c4b5d6
--- /dev/null
+++ b/Server/DB/Migrations/20231114184404_AddGuildData.Designer.cs
@@ -0,0 +1,327 @@
+//
+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("20231114184404_AddGuildData")]
+ partial class AddGuildData
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "7.0.13")
+ .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.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.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("ItemId")
+ .HasColumnType("integer");
+
+ b.Property("ItemType")
+ .HasColumnType("smallint");
+
+ 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.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.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/20231114184404_AddGuildData.cs b/Server/DB/Migrations/20231114184404_AddGuildData.cs
new file mode 100644
index 0000000..1f93d59
--- /dev/null
+++ b/Server/DB/Migrations/20231114184404_AddGuildData.cs
@@ -0,0 +1,104 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Server.DB.Migrations;
+
+///
+public partial class AddGuildData : Migration
+{
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "GuildId",
+ table: "Characters",
+ type: "uuid",
+ nullable: false,
+ defaultValue: new Guid("00000000-0000-0000-0000-000000000000"));
+
+ migrationBuilder.CreateTable(
+ name: "Guild",
+ columns: table => new
+ {
+ Id = table.Column(type: "uuid", nullable: false),
+ Name = table.Column(type: "text", nullable: true),
+ Notice = table.Column(type: "text", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Guild", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "GuildMember",
+ columns: table => new
+ {
+ Id = table.Column(type: "uuid", nullable: false),
+ CharacterId = table.Column(type: "uuid", nullable: false),
+ GuildId = table.Column(type: "uuid", nullable: false),
+ Rank = table.Column(type: "smallint", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_GuildMember", x => x.Id);
+ table.ForeignKey(
+ name: "FK_GuildMember_Characters_CharacterId",
+ column: x => x.CharacterId,
+ principalTable: "Characters",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "FK_GuildMember_Guild_GuildId",
+ column: x => x.GuildId,
+ principalTable: "Guild",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Characters_GuildId",
+ table: "Characters",
+ column: "GuildId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_GuildMember_CharacterId",
+ table: "GuildMember",
+ column: "CharacterId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_GuildMember_GuildId",
+ table: "GuildMember",
+ column: "GuildId");
+
+ migrationBuilder.AddForeignKey(
+ name: "FK_Characters_Guild_GuildId",
+ table: "Characters",
+ column: "GuildId",
+ principalTable: "Guild",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropForeignKey(
+ name: "FK_Characters_Guild_GuildId",
+ table: "Characters");
+
+ migrationBuilder.DropTable(
+ name: "GuildMember");
+
+ migrationBuilder.DropTable(
+ name: "Guild");
+
+ migrationBuilder.DropIndex(
+ name: "IX_Characters_GuildId",
+ table: "Characters");
+
+ migrationBuilder.DropColumn(
+ name: "GuildId",
+ table: "Characters");
+ }
+}
diff --git a/Server/DB/Migrations/WonderkingContextModelSnapshot.cs b/Server/DB/Migrations/WonderkingContextModelSnapshot.cs
index 51f1e73..ab1f034 100644
--- a/Server/DB/Migrations/WonderkingContextModelSnapshot.cs
+++ b/Server/DB/Migrations/WonderkingContextModelSnapshot.cs
@@ -17,7 +17,7 @@ namespace Server.DB.Migrations
{
#pragma warning disable 612, 618
modelBuilder
- .HasAnnotation("ProductVersion", "7.0.10")
+ .HasAnnotation("ProductVersion", "7.0.13")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
@@ -91,6 +91,58 @@ namespace Server.DB.Migrations
b.ToTable("Characters");
});
+ 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("ItemId")
+ .HasColumnType("smallint");
+
+ b.Property("ItemType")
+ .HasColumnType("smallint");
+
+ 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")
@@ -102,10 +154,26 @@ namespace Server.DB.Migrations
b.Navigation("Account");
});
+ 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");
+ });
#pragma warning restore 612, 618
}
}
diff --git a/Server/DB/WonderkingContext.cs b/Server/DB/WonderkingContext.cs
index 2cc9720..94efe19 100644
--- a/Server/DB/WonderkingContext.cs
+++ b/Server/DB/WonderkingContext.cs
@@ -32,10 +32,20 @@ public class WonderkingContext : DbContext
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();
+ 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.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/LoggerMessages/LoginHandlerLoggerMessages.cs b/Server/LoggerMessages/LoginHandlerLoggerMessages.cs
new file mode 100644
index 0000000..adefe33
--- /dev/null
+++ b/Server/LoggerMessages/LoginHandlerLoggerMessages.cs
@@ -0,0 +1,15 @@
+using Microsoft.Extensions.Logging;
+using Server.PacketHandlers;
+
+namespace Server.LoggerMessages;
+
+public static partial class LoginHandlerLoggerMessages
+{
+ [LoggerMessage(EventId = 0, Level = LogLevel.Information,
+ Message = "Login data: Username {Username} & Password {Password}")]
+ public static partial void LoginData(this ILogger logger, string username, string password);
+
+ [LoggerMessage(EventId = 1, Level = LogLevel.Information,
+ Message = "Requested account for user: {Username} does not exist")]
+ public static partial void RequestedAccountDoesNotExist(this ILogger logger, string username);
+}
diff --git a/Server/PacketHandlers/ChannelSelectionHandler.cs b/Server/PacketHandlers/ChannelSelectionHandler.cs
index 12c2030..de92f14 100644
--- a/Server/PacketHandlers/ChannelSelectionHandler.cs
+++ b/Server/PacketHandlers/ChannelSelectionHandler.cs
@@ -1,5 +1,8 @@
using Microsoft.EntityFrameworkCore;
+using Server.DB.Documents;
+using Wonderking.Game.Data.Character;
using Wonderking.Packets.Incoming;
+using Wonderking.Packets.Outgoing;
namespace Server.PacketHandlers;
@@ -29,9 +32,148 @@ public class ChannelSelectionHandler : IPacketHandler
public Task HandleAsync(ChannelSelectionPacket packet, TcpSession session)
{
var authSession = (AuthSession)session;
- var charactersOfAccount = this._wonderkingContext.Accounts.Include(account => account.Characters)
- .FirstOrDefault(a => a.Id == authSession.AccountId)
- ?.Characters;
+ ChannelSelectionResponsePacket responsePacket;
+ CharacterSelectionSetGuildNamePacket guildNameResponsePacket;
+
+ var hasCharacters = this._wonderkingContext.Accounts.Include(account => account.Characters)
+ .FirstOrDefault(a => a.Id == authSession.AccountId)?.Characters.Count != 0;
+ var testingChars = false;
+ if (hasCharacters && !testingChars)
+ {
+ responsePacket = new ChannelSelectionResponsePacket
+ {
+ ChannelIsFullFlag = 0,
+ Endpoint = "127.0.0.1",
+ Port = 12345,
+ Characters = this._wonderkingContext.Characters.Where(c => c.AccountId == authSession.AccountId)
+ .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.ItemType == ItemType.WornEquipment)
+ .Select(item => item.ItemId)
+ .ToArray(),
+ EquippedCashItems = c.InventoryItems
+ .Where(item => item.ItemType == ItemType.WornCashEquipment)
+ .Select(item => item.ItemId)
+ .ToArray(),
+ })
+ .ToArray(),
+ };
+
+ guildNameResponsePacket = new CharacterSelectionSetGuildNamePacket
+ {
+ GuildNames = this._wonderkingContext.Characters.Where(c => c.AccountId == authSession.AccountId)
+ .Select(character => character.Guild.Name).ToArray()
+ };
+ }
+ else
+ {
+ responsePacket = testingChars
+ ? CreateTestChannelSelectionResponsePacket()
+ : new ChannelSelectionResponsePacket
+ {
+ ChannelIsFullFlag = 0,
+ Endpoint = "127.0.0.1",
+ Port = 12345,
+ Characters = Array.Empty()
+ };
+ guildNameResponsePacket = new CharacterSelectionSetGuildNamePacket
+ {
+ GuildNames = new[] { "ABCDEFGHIJKLMNOP", "QRSTUVWXYZ123456", "A Guild Name For" }
+ };
+ }
+
+ authSession.Send(responsePacket);
+ if (guildNameResponsePacket.GuildNames.Length > 0)
+ {
+ authSession.Send(guildNameResponsePacket);
+ }
+
return Task.CompletedTask;
}
+
+ private static ChannelSelectionResponsePacket CreateTestChannelSelectionResponsePacket()
+ {
+ return new ChannelSelectionResponsePacket
+ {
+ ChannelIsFullFlag = 0,
+ Endpoint = "127.0.0.1",
+ Port = 12345,
+ Characters = new[]
+ {
+ new CharacterData
+ {
+ Name = "1",
+ Job = new JobData { FirstJob = 1, SecondJob = 0, ThirdJob = 0, FourthJob = 0 },
+ Gender = Gender.Female,
+ Level = ushort.MaxValue - 1,
+ Experience = 255,
+ Stats = new BaseStats
+ {
+ Strength = 5,
+ Dexterity = 5,
+ Intelligence = 5,
+ Vitality = 5,
+ Luck = 5,
+ Wisdom = 5
+ },
+ Health = int.MaxValue - 1,
+ Mana = int.MaxValue - 1,
+ EquippedItems = Enumerable.Repeat((ushort)25, 20).ToArray(),
+ EquippedCashItems = Enumerable.Repeat((ushort)70, 20).ToArray()
+ },
+ new CharacterData
+ {
+ Name = "2",
+ Job = new JobData { FirstJob = 1, SecondJob = 0, ThirdJob = 0, FourthJob = 0 },
+ Gender = Gender.Female,
+ Level = ushort.MaxValue - 1,
+ Experience = 255,
+ Stats = new BaseStats
+ {
+ Strength = 5,
+ Dexterity = 5,
+ Intelligence = 5,
+ Vitality = 5,
+ Luck = 5,
+ Wisdom = 5
+ },
+ Health = int.MaxValue - 1,
+ Mana = int.MaxValue - 1,
+ EquippedItems = Enumerable.Repeat((ushort)35, 20).ToArray(),
+ EquippedCashItems = Enumerable.Repeat((ushort)55, 20).ToArray()
+ },
+ new CharacterData
+ {
+ Name = "3",
+ Job = new JobData { FirstJob = 1, SecondJob = 0, ThirdJob = 0, FourthJob = 0 },
+ Gender = Gender.Female,
+ Level = ushort.MaxValue - 1,
+ Experience = 255,
+ Stats = new BaseStats
+ {
+ Strength = 5,
+ Dexterity = 5,
+ Intelligence = 5,
+ Vitality = 5,
+ Luck = 5,
+ Wisdom = 5
+ },
+ Health = int.MaxValue - 1,
+ Mana = int.MaxValue - 1,
+ EquippedItems = Enumerable.Repeat((ushort)45, 20).ToArray(),
+ EquippedCashItems = Enumerable.Repeat((ushort)65, 20).ToArray()
+ }
+ },
+ };
+ }
}
diff --git a/Server/PacketHandlers/LoginHandler.cs b/Server/PacketHandlers/LoginHandler.cs
index 35dd997..c56c977 100644
--- a/Server/PacketHandlers/LoginHandler.cs
+++ b/Server/PacketHandlers/LoginHandler.cs
@@ -1,5 +1,6 @@
using System.Security.Cryptography;
using System.Text;
+using Server.LoggerMessages;
using Wonderking.Packets.Incoming;
using Wonderking.Packets.Outgoing;
@@ -25,47 +26,79 @@ public class LoginHandler : IPacketHandler
this._configuration = configuration;
}
- public async Task HandleAsync(LoginInfoPacket packet, TcpSession session)
+ private static Task GetPasswordHashAsync(string password, byte[] salt, Guid userId)
{
- LoginResponseReason loginResponseReason;
- this._logger.LogInformation("Login data: Username {Username} & Password {Password}", packet.Username,
- packet.Password);
- var account = this._wonderkingContext.Accounts.FirstOrDefault(a => a.Username == packet.Username);
-
// https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Chea1t_Sheet.html#argon2id
// "Use Argon2id with a minimum configuration of 19 MiB of memory, an iteration count of 2, and 1 degree of parallelism."
- var argon2Id = new Argon2id(Encoding.ASCII.GetBytes(packet.Password))
+ var argon2Id = new Argon2id(Encoding.ASCII.GetBytes(password))
{
MemorySize = 1024 * 19,
Iterations = 2,
- DegreeOfParallelism = 1
+ DegreeOfParallelism = 1,
+ Salt = salt,
+ AssociatedData = userId.ToByteArray()
};
+ return argon2Id.GetBytesAsync(16);
+ }
+
+ private async Task CreateAccountOnLoginAsync(string username, string password)
+ {
+ LoginResponseReason loginResponseReason;
+ var transaction =
+ await _wonderkingContext.Database.BeginTransactionAsync().ConfigureAwait(true);
+ await using (transaction.ConfigureAwait(false))
+ {
+ try
+ {
+ var salt = RandomNumberGenerator.GetBytes(16);
+ var finalAccount =
+ await this._wonderkingContext.Accounts.AddAsync(new Account(username,
+ Array.Empty(), "",
+ 0, salt)).ConfigureAwait(true);
+ await this._wonderkingContext.SaveChangesAsync().ConfigureAwait(true);
+ finalAccount.Entity.Password =
+ await LoginHandler.GetPasswordHashAsync(password, salt, finalAccount.Entity.Id)
+ .ConfigureAwait(true);
+ this._wonderkingContext.Accounts.Update(finalAccount.Entity);
+ loginResponseReason = LoginResponseReason.Ok;
+ await this._wonderkingContext.SaveChangesAsync().ConfigureAwait(true);
+
+ await transaction.CommitAsync().ConfigureAwait(true);
+ }
+ catch (Exception)
+ {
+ await transaction.RollbackAsync().ConfigureAwait(true); // Rollback the transaction on error
+ throw;
+ }
+ }
+
+ return loginResponseReason;
+ }
+
+ public async Task HandleAsync(LoginInfoPacket packet, TcpSession session)
+ {
+ LoginResponseReason loginResponseReason;
+ this._logger.LoginData(packet.Username, packet.Password);
+ var account = this._wonderkingContext.Accounts.FirstOrDefault(a => a.Username == packet.Username);
+
if (account == null)
{
if (this._configuration.GetSection("Testing").GetValue("CreateAccountOnLogin"))
{
- argon2Id.Salt = RandomNumberGenerator.GetBytes(16);
- var finalAccount =
- await this._wonderkingContext.Accounts.AddAsync(new Account(packet.Username, Array.Empty(), "",
- 0, argon2Id.Salt)).ConfigureAwait(true);
- await this._wonderkingContext.SaveChangesAsync().ConfigureAwait(true);
- argon2Id.AssociatedData = finalAccount.Entity.Id.ToByteArray();
- finalAccount.Entity.Password = await argon2Id.GetBytesAsync(16).ConfigureAwait(true);
- this._wonderkingContext.Accounts.Update(finalAccount.Entity);
- loginResponseReason = LoginResponseReason.Ok;
- await this._wonderkingContext.SaveChangesAsync().ConfigureAwait(true);
+ loginResponseReason = await CreateAccountOnLoginAsync(packet.Username, packet.Password)
+ .ConfigureAwait(true);
}
else
{
- this._logger.LogInformation("Requested account for user: {Username} does not exist", packet.Username);
+ this._logger.RequestedAccountDoesNotExist(packet.Username);
loginResponseReason = LoginResponseReason.AccountDoesNotExit;
}
}
else
{
- argon2Id.Salt = account.Salt;
- argon2Id.AssociatedData = account.Id.ToByteArray();
- var tempPasswordBytes = await argon2Id.GetBytesAsync(16).ConfigureAwait(true);
+ var salt = account.Salt;
+ var tempPasswordBytes = await LoginHandler.GetPasswordHashAsync(packet.Password, salt, account.Id)
+ .ConfigureAwait(false);
loginResponseReason = tempPasswordBytes.SequenceEqual(account.Password)
? LoginResponseReason.Ok
: LoginResponseReason.WrongPassword;
@@ -84,6 +117,7 @@ public class LoginHandler : IPacketHandler
sess.AccountId = account.Id;
}
+ _logger.LogInformation("LoginResponsePacket: {@LoginResponsePacket}", loginResponsePacket);
sess?.Send(loginResponsePacket);
}
}
diff --git a/Server/Program.cs b/Server/Program.cs
index 3241823..70ff82c 100644
--- a/Server/Program.cs
+++ b/Server/Program.cs
@@ -24,6 +24,7 @@ builder.Services.AddDbContext();
builder.Services.AddSingleton();
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/Server.csproj b/Server/Server.csproj
index 0dfbb16..ba239ce 100644
--- a/Server/Server.csproj
+++ b/Server/Server.csproj
@@ -8,7 +8,7 @@
Server
default
true
- net8.0;net7.0
+ net8.0
true
strict
Timothy (RaiNote) Schenk
@@ -41,17 +41,17 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
-
-
+
+
-
-
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
@@ -63,16 +63,20 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
@@ -89,6 +93,7 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
+
diff --git a/Server/Services/ItemObjectPoolService.cs b/Server/Services/ItemObjectPoolService.cs
new file mode 100644
index 0000000..3a8ac45
--- /dev/null
+++ b/Server/Services/ItemObjectPoolService.cs
@@ -0,0 +1,51 @@
+using System.Collections.Concurrent;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Hosting;
+using Wonderking.Game.Data;
+using Wonderking.Game.Reader;
+
+namespace Server.Services;
+
+public class ItemObjectPoolService : IHostedService
+{
+ readonly ConcurrentDictionary _itemObjectPool = new();
+ private readonly ItemReader _itemReader;
+
+ public ItemObjectPoolService(IConfiguration configuration)
+ {
+ _itemReader = new ItemReader(configuration.GetSection("Game").GetSection("Data").GetValue("Path") ??
+ string.Empty);
+ }
+
+ public Task StartAsync(CancellationToken cancellationToken)
+ {
+ var amountOfEntries = _itemReader.GetAmountOfEntries();
+ ParallelEnumerable.Range(0, (int)amountOfEntries).AsParallel().ForAll(i =>
+ {
+ var itemObject = _itemReader.GetEntry((uint)i);
+ _itemObjectPool.TryAdd(itemObject.ItemID, itemObject);
+ });
+ return Task.CompletedTask;
+ }
+
+ public Task StopAsync(CancellationToken cancellationToken)
+ {
+ _itemReader.Dispose();
+ return Task.CompletedTask;
+ }
+
+ public ItemObject GetItem(ushort itemId)
+ {
+ return _itemObjectPool[itemId];
+ }
+
+ public bool ContainsItem(ushort itemId)
+ {
+ return _itemObjectPool.ContainsKey(itemId);
+ }
+
+ public IQueryable QueryItems()
+ {
+ return _itemObjectPool.AsReadOnly().Values.AsQueryable();
+ }
+}
diff --git a/Server/Services/PacketDistributorService.cs b/Server/Services/PacketDistributorService.cs
index 79f9c34..5bc2d43 100644
--- a/Server/Services/PacketDistributorService.cs
+++ b/Server/Services/PacketDistributorService.cs
@@ -36,9 +36,9 @@ public class PacketDistributorService : IHostedService
var tempDeserializationMap =
new Dictionary>();
- var executingAssembly = Assembly.GetExecutingAssembly();
- var packetsTypes = this.GetPacketsWithId(executingAssembly);
- var packetHandlers = this.GetAllPacketHandlersWithId(executingAssembly);
+ var wonderkingAssembly = Assembly.GetAssembly(typeof(IPacket));
+ var packetsTypes = this.GetPacketsWithId(wonderkingAssembly);
+ var packetHandlers = this.GetAllPacketHandlersWithId(Assembly.GetExecutingAssembly());
this._packetHandlersInstantiation = new ConcurrentDictionary();
packetHandlers.ForEach(x =>
{
@@ -76,6 +76,7 @@ public class PacketDistributorService : IHostedService
// ! : We are filtering if types that don't have an instance of the required Attribute
var packetsWithId = executingAssembly.GetTypes().AsParallel()
.Where(type => type.HasInterface(typeof(IPacket)) && type is { IsInterface: false, IsAbstract: false })
+ .Where(type => type.Namespace?.Contains("Incoming") ?? false)
.Select(type => new { Type = type, Attribute = type.GetCustomAttribute() })
.Where(item => item.Attribute is not null)
.ToDictionary(item => item.Attribute!.Code, item => item.Type);
diff --git a/Server/docker-compose.yml b/Server/docker-compose.yml
index 1efa332..1f8fa76 100644
--- a/Server/docker-compose.yml
+++ b/Server/docker-compose.yml
@@ -1,7 +1,8 @@
services:
server:
container_name: continuity-server
- image: server:latest
+ image: continuity:latest
+ restart: always
depends_on:
- db
environment:
@@ -11,29 +12,39 @@
- DB:Port=5432
- DB:Username=continuity
- DB:Password=continuity
+ - Game:Data:Path=/app/data
networks:
- continuity
ports:
- "10001:10001"
+ volumes:
+ - type: bind
+ source: game-data
+ target: /app/data
+ read_only: true
db:
container_name: continuity-db
image: postgres:16.0-alpine
+ restart: always
environment:
- POSTGRES_USER=continuity
- POSTGRES_DB=continuity
- POSTGRES_PASSWORD=continuity
networks:
- continuity
+ ports:
+ - "5432:5432"
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
- test: [ "CMD-SHELL", "sh -c 'pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}'" ]
+ test: [ "CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}" ]
interval: 10s
timeout: 3s
retries: 3
networks:
continuity:
+
volumes:
db-data:
diff --git a/Server/settings.Development.json b/Server/settings.Development.json
index 1b64c70..ded1040 100644
--- a/Server/settings.Development.json
+++ b/Server/settings.Development.json
@@ -8,5 +8,10 @@
},
"Testing": {
"CreateAccountOnLogin": true
+ },
+ "Game":{
+ "Data":{
+ "Path": "../wk-data"
+ }
}
}
diff --git a/Server/DB/Documents/Gender.cs b/Wonderking/Game/Data/Character/Gender.cs
similarity index 63%
rename from Server/DB/Documents/Gender.cs
rename to Wonderking/Game/Data/Character/Gender.cs
index 441de51..49f9949 100644
--- a/Server/DB/Documents/Gender.cs
+++ b/Wonderking/Game/Data/Character/Gender.cs
@@ -1,4 +1,4 @@
-namespace Server.DB.Documents;
+namespace Wonderking.Game.Data.Character;
public enum Gender : byte
{
diff --git a/Server/DB/Documents/PvPLevel.cs b/Wonderking/Game/Data/Character/PvPLevel.cs
similarity index 92%
rename from Server/DB/Documents/PvPLevel.cs
rename to Wonderking/Game/Data/Character/PvPLevel.cs
index d1bb1ec..8fbaca4 100644
--- a/Server/DB/Documents/PvPLevel.cs
+++ b/Wonderking/Game/Data/Character/PvPLevel.cs
@@ -1,4 +1,4 @@
-namespace Server.DB.Documents;
+namespace Wonderking.Game.Data.Character;
public enum PvPLevel : byte
{
diff --git a/Wonderking/Game/Data/Item/ContainedItem.cs b/Wonderking/Game/Data/Item/ContainedItem.cs
new file mode 100644
index 0000000..3f819f4
--- /dev/null
+++ b/Wonderking/Game/Data/Item/ContainedItem.cs
@@ -0,0 +1,10 @@
+using System.Runtime.InteropServices;
+
+namespace Wonderking.Game.Data.Item;
+
+[StructLayout(LayoutKind.Sequential)]
+public struct ContainedItem
+{
+ public short ID { get; internal set; }
+ public float ObtainChance { get; internal set; }
+}
diff --git a/Wonderking/Game/Data/Item/CraftMaterial.cs b/Wonderking/Game/Data/Item/CraftMaterial.cs
new file mode 100644
index 0000000..b548e36
--- /dev/null
+++ b/Wonderking/Game/Data/Item/CraftMaterial.cs
@@ -0,0 +1,11 @@
+using System.Runtime.InteropServices;
+
+namespace Wonderking.Game.Data.Item;
+
+[StructLayout(LayoutKind.Sequential)]
+public struct CraftMaterial
+{
+ public uint ID;
+ public uint Amount;
+}
+
diff --git a/Wonderking/Game/Data/Item/ElementalStats.cs b/Wonderking/Game/Data/Item/ElementalStats.cs
new file mode 100644
index 0000000..e56739b
--- /dev/null
+++ b/Wonderking/Game/Data/Item/ElementalStats.cs
@@ -0,0 +1,55 @@
+using System.Runtime.InteropServices;
+
+namespace Wonderking.Game.Data.Item;
+
+[StructLayout(LayoutKind.Explicit, Size = 64)]
+public struct ElementalStats
+{
+ [FieldOffset(0), MarshalAs(UnmanagedType.I4)]
+ public int MinimumFireDamage;
+
+ [FieldOffset(4), MarshalAs(UnmanagedType.I4)]
+ public int MinimumWaterDamage;
+
+ [FieldOffset(8), MarshalAs(UnmanagedType.I4)]
+ public int MinimumDarkDamage;
+
+ [FieldOffset(12), MarshalAs(UnmanagedType.I4)]
+ public int MinimumHolyDamage;
+
+ [FieldOffset(16), MarshalAs(UnmanagedType.I4)]
+ public int MaximumFireDamage;
+
+ [FieldOffset(20), MarshalAs(UnmanagedType.I4)]
+ public int MaximumWaterDamage;
+
+ [FieldOffset(24), MarshalAs(UnmanagedType.I4)]
+ public int MaximumDarkDamage;
+
+ [FieldOffset(28), MarshalAs(UnmanagedType.I4)]
+ public int MaximumHolyDamage;
+
+ [FieldOffset(32), MarshalAs(UnmanagedType.U4)]
+ public uint ElementFire;
+
+ [FieldOffset(36), MarshalAs(UnmanagedType.U4)]
+ public uint ElementWater;
+
+ [FieldOffset(40), MarshalAs(UnmanagedType.U4)]
+ public uint ElementDark;
+
+ [FieldOffset(44), MarshalAs(UnmanagedType.U4)]
+ public uint ElementHoly;
+
+ [FieldOffset(48), MarshalAs(UnmanagedType.I4)]
+ public int FireResistance;
+
+ [FieldOffset(52), MarshalAs(UnmanagedType.I4)]
+ public int WaterResistance;
+
+ [FieldOffset(56), MarshalAs(UnmanagedType.I4)]
+ public int DarkResistance;
+
+ [FieldOffset(60), MarshalAs(UnmanagedType.I4)]
+ public int HolyResistance;
+}
diff --git a/Wonderking/Game/Data/Item/ItemOptions.cs b/Wonderking/Game/Data/Item/ItemOptions.cs
new file mode 100644
index 0000000..3b52fb1
--- /dev/null
+++ b/Wonderking/Game/Data/Item/ItemOptions.cs
@@ -0,0 +1,7 @@
+namespace Wonderking.Game.Data.Item;
+
+public struct ItemOptions
+{
+ public ICollection OptionIDs { get; internal set; }
+ public bool OptionAvailable { get; internal set; }
+}
diff --git a/Wonderking/Game/Data/Item/Stats.cs b/Wonderking/Game/Data/Item/Stats.cs
new file mode 100644
index 0000000..009fa3d
--- /dev/null
+++ b/Wonderking/Game/Data/Item/Stats.cs
@@ -0,0 +1,25 @@
+using System.Runtime.InteropServices;
+
+namespace Wonderking.Game.Data.Item;
+
+[StructLayout(LayoutKind.Explicit, Size = 24)]
+public struct Stats
+{
+ [FieldOffset(0), MarshalAs(UnmanagedType.I4)]
+ public int Strength;
+
+ [FieldOffset(4), MarshalAs(UnmanagedType.I4)]
+ public int Dexterity;
+
+ [FieldOffset(8), MarshalAs(UnmanagedType.I4)]
+ public int Intelligence;
+
+ [FieldOffset(12), MarshalAs(UnmanagedType.I4)]
+ public int Vitality;
+
+ [FieldOffset(16), MarshalAs(UnmanagedType.I4)]
+ public int Luck;
+
+ [FieldOffset(20), MarshalAs(UnmanagedType.I4)]
+ public int Wisdom;
+}
diff --git a/Wonderking/Game/Data/ItemObject.cs b/Wonderking/Game/Data/ItemObject.cs
new file mode 100644
index 0000000..4284d2f
--- /dev/null
+++ b/Wonderking/Game/Data/ItemObject.cs
@@ -0,0 +1,170 @@
+using System.Text.Json.Serialization;
+using Wonderking.Game.Data.Item;
+using Wonderking.Utils;
+
+namespace Wonderking.Game.Data;
+
+[GameDataMetadata(932, "baseitemdata.dat", 197)]
+public struct ItemObject
+{
+ public uint ItemID { get; set; }
+ public bool Disabled { get; set; }
+ public uint ItemType { get; set; }
+
+ [JsonConverter(typeof(ByteArrayConverter))]
+ public byte[] Unknown2 { get; set; }
+
+ [JsonConverter(typeof(ByteArrayConverter))]
+ public byte[] Unknown3 { get; set; }
+
+ public uint ClassNo1 { get; set; }
+ public uint ClassNo2 { get; set; }
+ public uint ClassNo3 { get; set; }
+ public uint ClassNo4 { get; set; }
+ public uint SlotNo1 { get; set; }
+ public uint SlotNo2 { get; set; }
+
+ [JsonConverter(typeof(ByteArrayConverter))]
+ public byte[] Unknown4 { get; set; }
+
+ public uint IsCash { get; set; }
+
+ [JsonConverter(typeof(ByteArrayConverter))]
+ public byte[] Unknown5 { get; set; }
+
+ public uint Price { get; set; }
+
+ [JsonConverter(typeof(ByteArrayConverter))]
+ public byte[] Unknown7 { get; set; }
+
+ public uint MaxNumber { get; set; }
+
+ [JsonConverter(typeof(ByteArrayConverter))]
+ public byte[] Unknown17 { get; set; }
+
+ public uint MaximumLevelRequirement { get; set; }
+ public uint SexNo { get; set; }
+ public uint WeaponSomething { get; set; }
+
+ [JsonConverter(typeof(ByteArrayConverter))]
+ public byte[] Unknown8 { get; set; }
+
+ [JsonConverter(typeof(ByteArrayConverter))]
+ public byte[] R2C { get; set; }
+
+ [JsonConverter(typeof(ByteArrayConverter))]
+ public byte[] Unknown9 { get; set; }
+
+ public Stats Stats { get; set; }
+ public ElementalStats ElementalStats { get; set; }
+
+ [JsonConverter(typeof(ByteArrayConverter))]
+ public byte[] R7C { get; set; }
+
+ [JsonConverter(typeof(ByteArrayConverter))]
+ public byte[] R8C { get; set; }
+
+ public float Speed { get; set; }
+
+ public float Jump { get; set; }
+ public int StatDefense { get; set; }
+ public uint MagicID { get; set; }
+
+ [JsonConverter(typeof(ByteArrayConverter))]
+ public byte[] Unknown13 { get; set; }
+
+ [JsonConverter(typeof(ByteArrayConverter))]
+ public byte[] Unknown14 { get; set; }
+
+ public int AdditionalHealthRecoveryVolume { get; set; }
+
+ [JsonConverter(typeof(ByteArrayConverter))]
+ public byte[] R9C_1 { get; set; }
+
+ public int AdditionalManaRecoveryVolume { get; set; }
+
+ [JsonConverter(typeof(ByteArrayConverter))]
+ public byte[] R9C_2 { get; set; }
+
+ [JsonConverter(typeof(ByteArrayConverter))]
+ public byte[] R10C { get; set; }
+
+ public int AdditionalHealthPoints { get; set; }
+ public int AdditionalManaPoints { get; set; }
+ public bool IsArrow { get; set; }
+
+ [JsonConverter(typeof(ByteArrayConverter))]
+ public byte[] Unknown18 { get; set; }
+
+ public int AdditionalEvasionRate { get; set; }
+ public int HitRate { get; set; }
+
+ public int ChanceToHit { get; set; }
+ public int MagicalDamage { get; set; }
+ public int CriticalHitChance { get; set; }
+
+ [JsonConverter(typeof(ByteArrayConverter))]
+ public byte[] R12C { get; set; }
+
+ [JsonConverter(typeof(ByteArrayConverter))]
+ public byte[] Unknown16 { get; set; }
+
+ public int MinimalAttackDamage { get; set; }
+ public int MaximalAttackDamage { get; set; }
+ public int PhysicalDamage { get; set; }
+ public CraftMaterial[] CraftMaterial { get; set; }
+ public uint CraftResultAmount { get; set; }
+
+ [JsonConverter(typeof(ByteArrayConverter))]
+ public byte[] R14C { get; set; }
+
+ public uint CraftResultItem { get; set; }
+
+ [JsonConverter(typeof(ByteArrayConverter))]
+ public byte[] R15C { get; set; }
+
+ [JsonConverter(typeof(ByteArrayConverter))]
+ public byte[] R16C { get; set; }
+
+ public int InventoryX { get; set; }
+ public int InventoryY { get; set; }
+ public int InventoryWidth { get; set; }
+ public int InventoryHeight { get; set; }
+ public int SheetID { get; set; }
+ public string Name { get; set; }
+ public string Description { get; set; }
+
+ [JsonConverter(typeof(ByteArrayConverter))]
+ public byte[] Unknown1 { get; set; }
+
+ public bool IsEnchantable { get; set; }
+
+ public uint SetID { get; set; }
+ public uint[] SetItems { get; set; }
+
+ [JsonConverter(typeof(ByteArrayConverter))]
+ public byte[] Unknown1_2 { get; set; }
+
+ public ItemOptions Options { get; set; }
+
+ [JsonConverter(typeof(ByteArrayConverter))]
+ public byte[] Unknown19 { get; set; }
+
+ public byte PetID { get; set; }
+
+ [JsonConverter(typeof(ByteArrayConverter))]
+ public byte[] Unknown20 { get; set; }
+
+ public byte HitBoxScaling { get; set; }
+
+ [JsonConverter(typeof(ByteArrayConverter))]
+ public byte[] Unknown20_2 { get; set; }
+
+ public ContainedItem[] ContainedItems { get; set; }
+
+ public bool IsQuestItem { get; set; }
+ public byte MinimumLevelRequirement { get; set; }
+
+ [JsonConverter(typeof(ByteArrayConverter))]
+ public byte[] Unknown21_2 { get; set; }
+}
diff --git a/Wonderking/Game/DataReader.cs b/Wonderking/Game/DataReader.cs
new file mode 100644
index 0000000..e67c8a4
--- /dev/null
+++ b/Wonderking/Game/DataReader.cs
@@ -0,0 +1,50 @@
+using System.Reflection;
+
+namespace Wonderking.Game;
+
+public abstract class DataReader
+{
+ protected DataReader(string path)
+ {
+ Path = path;
+ DatFileContent = new(GetDatFileContent(path).ToArray());
+ }
+
+ private protected string Path { get; init; }
+
+ public abstract uint GetAmountOfEntries();
+ public abstract T GetEntry(uint entryId);
+
+ protected ushort GetSizeOfEntry()
+ {
+ return typeof(T).GetCustomAttribute()?.DataEntrySize ??
+ throw new NotSupportedException("DataEntrySize is null");
+ }
+
+ private static string GetDatFileName()
+ {
+ return typeof(T).GetCustomAttribute()?.DatFileName ??
+ throw new NotSupportedException("DatFileName is null");
+ }
+
+ private static byte GetXorKey()
+ {
+ return typeof(T).GetCustomAttribute()?.XorKey ??
+ throw new NotSupportedException("XorKey is null");
+ }
+
+ protected MemoryStream DatFileContent { get; }
+
+ private static Span GetDatFileContent(string path)
+ {
+ var fileData = File.ReadAllBytes(path + GetDatFileName());
+ var data = new byte[fileData.Length];
+
+ for (var i = 0; i < fileData.Length; i++)
+ {
+ data[i] = (byte)(fileData[i] ^ GetXorKey());
+ }
+
+ return data;
+ }
+}
diff --git a/Wonderking/Game/GameDataMetadataAttribute.cs b/Wonderking/Game/GameDataMetadataAttribute.cs
new file mode 100644
index 0000000..b9db18c
--- /dev/null
+++ b/Wonderking/Game/GameDataMetadataAttribute.cs
@@ -0,0 +1,12 @@
+using JetBrains.Annotations;
+
+namespace Wonderking.Game;
+
+[AttributeUsage(AttributeTargets.Struct | AttributeTargets.Class)]
+public class GameDataMetadataAttribute(ushort dataEntrySize, string datFileName, byte xorKey) : Attribute
+{
+ [UsedImplicitly] public byte XorKey { get; init; } = xorKey;
+ [UsedImplicitly] public ushort DataEntrySize { get; init; } = dataEntrySize;
+
+ [UsedImplicitly] internal string DatFileName { get; init; } = datFileName;
+}
diff --git a/Wonderking/Game/Reader/BinaryReader.cs b/Wonderking/Game/Reader/BinaryReader.cs
new file mode 100644
index 0000000..62c683d
--- /dev/null
+++ b/Wonderking/Game/Reader/BinaryReader.cs
@@ -0,0 +1,75 @@
+using System.Runtime.InteropServices;
+
+namespace Wonderking.Game.Reader;
+
+public static class BinaryReader where T : new()
+{
+ public static readonly Func Read;
+
+ static BinaryReader()
+ {
+ var type = typeof(T);
+
+ if (type == typeof(bool))
+ {
+ Read = (Func)(Delegate)(Func)(p => p.ReadBoolean());
+ }
+ else if (type == typeof(char))
+ {
+ Read = (Func)(Delegate)(Func)(p => p.ReadChar());
+ }
+ else if (type == typeof(string))
+ {
+ Read = (Func)(Delegate)(Func)(p => p.ReadString());
+ }
+ else if (type == typeof(sbyte))
+ {
+ Read = (Func)(Delegate)(Func)(p => p.ReadSByte());
+ }
+ else if (type == typeof(short))
+ {
+ Read = (Func)(Delegate)(Func)(p => p.ReadInt16());
+ }
+ else if (type == typeof(int))
+ {
+ Read = (Func)(Delegate)(Func)(p => p.ReadInt32());
+ }
+ else if (type == typeof(long))
+ {
+ Read = (Func)(Delegate)(Func)(p => p.ReadInt64());
+ }
+ else if (type == typeof(byte))
+ {
+ Read = (Func)(Delegate)(Func)(p => p.ReadByte());
+ }
+ else if (type == typeof(ushort))
+ {
+ Read = (Func)(Delegate)(Func)(p => p.ReadUInt16());
+ }
+ else if (type == typeof(uint))
+ {
+ Read = (Func)(Delegate)(Func)(p => p.ReadUInt32());
+ }
+ else if (type == typeof(ulong))
+ {
+ Read = (Func)(Delegate)(Func)(p => p.ReadUInt64());
+ }
+ else if (type == typeof(float))
+ {
+ Read = (Func)(Delegate)(Func)(p => p.ReadSingle());
+ }
+ else if (type == typeof(double))
+ {
+ Read = (Func)(Delegate)(Func)(p => p.ReadDouble());
+ }
+ else if (type == typeof(decimal))
+ {
+ Read = (Func)(Delegate)(Func)(p => p.ReadDecimal());
+ }
+ else
+ {
+ Read = (Func)(p =>
+ (T)(object)p.ReadBytes(Marshal.SizeOf(new T())));
+ }
+ }
+}
diff --git a/Wonderking/Game/Reader/GenericReaderExtensions.cs b/Wonderking/Game/Reader/GenericReaderExtensions.cs
new file mode 100644
index 0000000..17e888e
--- /dev/null
+++ b/Wonderking/Game/Reader/GenericReaderExtensions.cs
@@ -0,0 +1,37 @@
+
+/* Nicht gemergte Änderung aus Projekt "Wonderking(net7.0)"
+Vor:
+using System.Runtime.InteropServices;
+using System.Text;
+Nach:
+using System.Text;
+*/
+
+using System.Text;
+
+namespace Wonderking.Game.Reader;
+
+public static class GenericReaderExtensions
+{
+ public static string ReadString(this BinaryReader reader, int length)
+ {
+ var ret = Encoding.ASCII.GetString(reader.ReadBytes(length)).Replace("\0", "");
+ return ret;
+ }
+
+ public static T[] ReadArray(this BinaryReader pReader, int pLength) where T : new()
+ {
+ var array = new T[pLength];
+ for (var index = 0; index < pLength; ++index)
+ {
+ array[index] = pReader.Read();
+ }
+
+ return array;
+ }
+
+ public static T Read(this BinaryReader br) where T : new()
+ {
+ return BinaryReader.Read(br);
+ }
+}
diff --git a/Wonderking/Game/Reader/ItemReader.cs b/Wonderking/Game/Reader/ItemReader.cs
new file mode 100644
index 0000000..38ae8a3
--- /dev/null
+++ b/Wonderking/Game/Reader/ItemReader.cs
@@ -0,0 +1,107 @@
+using Wonderking.Game.Data;
+using Wonderking.Game.Data.Item;
+
+namespace Wonderking.Game.Reader;
+
+public class ItemReader(string path) : DataReader(path), IDisposable
+{
+ public override uint GetAmountOfEntries()
+ {
+ return (uint)((this.DatFileContent.Length - 9) / this.GetSizeOfEntry());
+ }
+
+ public override ItemObject GetEntry(uint entryId)
+ {
+ var item = new ItemObject();
+ this.DatFileContent.Position = 9 + entryId * this.GetSizeOfEntry();
+ var reader = new BinaryReader(this.DatFileContent);
+ item.ItemID = reader.ReadUInt32(); //9
+ item.Disabled = reader.ReadUInt32() == 1; //13
+ item.ItemType = reader.ReadUInt32(); //17
+ item.Unknown2 = reader.ReadBytes(4); //21
+ item.Unknown3 = reader.ReadBytes(4); //25
+ item.ClassNo1 = reader.ReadUInt32(); //29
+ item.ClassNo2 = reader.ReadUInt32(); //33
+ item.ClassNo3 = reader.ReadUInt32(); //37
+ item.ClassNo4 = reader.ReadUInt32(); //41
+ item.SlotNo1 = reader.ReadUInt32(); //45
+ item.SlotNo2 = reader.ReadUInt32(); //49
+ item.Unknown4 = reader.ReadBytes(4); //53
+ item.IsCash = reader.ReadUInt32(); //57
+ item.Unknown5 = reader.ReadBytes(4); //61
+ item.Price = reader.ReadUInt32(); //65
+ item.Unknown7 = reader.ReadBytes(4); //69
+ item.MaxNumber = reader.ReadUInt32(); //73
+ item.Unknown17 = reader.ReadBytes(12); //77
+ item.MaximumLevelRequirement = reader.ReadUInt32(); //89
+ item.SexNo = reader.ReadUInt32(); //93
+ item.WeaponSomething = reader.ReadUInt32(); //97
+ item.Unknown8 = reader.ReadBytes(4); //101
+ item.R2C = reader.ReadBytes(16); //105
+ item.Unknown9 = reader.ReadBytes(4); //121
+ item.Stats = reader.ReadStats(); //125
+ item.ElementalStats = reader.ReadElementalStats(); //149
+ item.R7C = reader.ReadBytes(4); //213
+ item.R8C = reader.ReadBytes(8); //217
+ item.Speed = reader.ReadSingle(); //225
+ item.Jump = reader.ReadSingle(); //229
+ item.StatDefense = reader.ReadInt32(); //233
+ item.MagicID = reader.ReadUInt32(); //237
+ item.Unknown13 = reader.ReadBytes(4); //241
+ item.Unknown14 = reader.ReadBytes(4); //245
+ item.AdditionalHealthRecoveryVolume = reader.ReadInt32(); //249
+ item.R9C_1 = reader.ReadBytes(4); //253
+ item.AdditionalManaRecoveryVolume = reader.ReadInt32(); //257
+ item.R9C_2 = reader.ReadBytes(4); //261
+ item.R10C = reader.ReadBytes(8); //265
+ item.AdditionalHealthPoints = reader.ReadInt32(); //273
+ item.AdditionalManaPoints = reader.ReadInt32(); //277
+ item.IsArrow = reader.ReadBoolean(); //281
+ item.Unknown18 = reader.ReadBytes(7); //282
+ item.AdditionalEvasionRate = reader.ReadInt32(); //289
+ item.HitRate = reader.ReadInt32(); //293
+ item.ChanceToHit = reader.ReadInt32(); //297
+ item.MagicalDamage = reader.ReadInt32(); //301
+ item.CriticalHitChance = reader.ReadInt32(); //305
+ item.R12C = reader.ReadBytes(4); //309
+ item.Unknown16 = reader.ReadBytes(4); //313
+ item.MinimalAttackDamage = reader.ReadInt32(); //317
+ item.MaximalAttackDamage = reader.ReadInt32(); //321
+ item.PhysicalDamage = reader.ReadInt32(); //325
+ item.CraftMaterial = reader.ReadCraftMaterial(); //329
+ item.CraftResultAmount = reader.ReadUInt32(); //361
+ item.R14C = reader.ReadBytes(4); //365
+ item.CraftResultItem = reader.ReadUInt32(); //369
+ item.R15C = reader.ReadBytes(4); //373
+ item.R16C = reader.ReadBytes(20); //377
+ item.InventoryX = reader.ReadInt32(); //397
+ item.InventoryY = reader.ReadInt32(); //401
+ item.InventoryWidth = reader.ReadInt32(); //405
+ item.InventoryHeight = reader.ReadInt32(); //409
+ item.SheetID = reader.ReadInt32(); //413
+ item.Name = reader.ReadString(20); //417
+ item.Description = reader.ReadString(85); //427
+ item.Unknown1 = reader.ReadBytes(175); //493
+ item.IsEnchantable = reader.ReadUInt32() == 1; //687
+ item.Unknown1_2 = reader.ReadBytes(104); //687
+ item.SetItems = reader.ReadArray(5);
+ item.SetID = reader.ReadUInt32(); //691
+ item.Options = reader.ReadItemOptions(); //819
+ item.Unknown19 = reader.ReadBytes(23); //835
+ item.PetID = reader.ReadByte(); //858
+ item.Unknown20 = reader.ReadBytes(20); //859
+ item.HitBoxScaling = reader.ReadByte(); //879
+ item.Unknown20_2 = reader.ReadBytes(13); //880
+ item.ContainedItems = reader.ReadContainedItems(); //893
+ item.IsQuestItem = reader.ReadBoolean(); //923
+ item.MinimumLevelRequirement = reader.ReadByte(); //924
+ item.Unknown21_2 = reader.ReadBytes(6); //925
+ reader.Dispose(); //931
+ return item;
+ }
+
+ public void Dispose()
+ {
+ this.DatFileContent.Dispose();
+ }
+}
diff --git a/Wonderking/Game/Reader/ItemReaderExtensions.cs b/Wonderking/Game/Reader/ItemReaderExtensions.cs
new file mode 100644
index 0000000..97a8b72
--- /dev/null
+++ b/Wonderking/Game/Reader/ItemReaderExtensions.cs
@@ -0,0 +1,96 @@
+using Wonderking.Game.Data.Item;
+
+namespace Wonderking.Game.Reader;
+
+public static class ItemReaderExtensions
+{
+ public static Stats ReadStats(this BinaryReader reader)
+ {
+ return new Stats()
+ {
+ Strength = reader.ReadInt32(), //125
+ Dexterity = reader.ReadInt32(), //129
+ Intelligence = reader.ReadInt32(), //133
+ Vitality = reader.ReadInt32(), //137
+ Luck = reader.ReadInt32(), //141
+ Wisdom = reader.ReadInt32(), //145
+ };
+ }
+
+ public static ElementalStats ReadElementalStats(this BinaryReader reader)
+ {
+ return new ElementalStats()
+ {
+ MinimumFireDamage = reader.ReadInt32(), //149
+ MinimumWaterDamage = reader.ReadInt32(), //153
+ MinimumDarkDamage = reader.ReadInt32(), //157
+ MinimumHolyDamage = reader.ReadInt32(), //161
+ MaximumFireDamage = reader.ReadInt32(), //165
+ MaximumWaterDamage = reader.ReadInt32(), //169
+ MaximumDarkDamage = reader.ReadInt32(), //173
+ MaximumHolyDamage = reader.ReadInt32(), //177
+ ElementFire = reader.ReadUInt32(), //181
+ ElementWater = reader.ReadUInt32(), //185
+ ElementDark = reader.ReadUInt32(), //189
+ ElementHoly = reader.ReadUInt32(), //193
+ FireResistance = reader.ReadInt32(), //197
+ WaterResistance = reader.ReadInt32(), //201
+ DarkResistance = reader.ReadInt32(), //205
+ HolyResistance = reader.ReadInt32(), //209
+ };
+ }
+
+ public static ContainedItem[] ReadContainedItems(this BinaryReader reader)
+ {
+ var list = new ContainedItem[5];
+ //893
+ for (var i = 0; i < 5; i++)
+ {
+ list[i].ID = reader.ReadInt16();
+ }
+
+ //903
+ for (var i = 0; i < 5; i++)
+ {
+ list[i].ObtainChance = reader.ReadSingle();
+ }
+
+ return list;
+ }
+
+ public static CraftMaterial[] ReadCraftMaterial(this BinaryReader reader)
+ {
+ var mats = new CraftMaterial[4];
+ //329
+ for (var i = 0; i < 4; ++i)
+ {
+ mats[i].ID = reader.ReadUInt32();
+ }
+
+ //345
+ for (var i = 0; i < 4; ++i)
+ {
+ mats[i].ID = reader.ReadUInt32();
+ }
+
+ return mats;
+ }
+
+ public static ItemOptions ReadItemOptions(this BinaryReader reader)
+ {
+ var options = new ItemOptions();
+
+ options.OptionAvailable = reader.ReadInt32() == 1; //819
+
+ var optionIDs = new List(4);
+ //823
+ for (var i = 0; i < 3; i++)
+ {
+ optionIDs.Add((byte)reader.ReadUInt32());
+ }
+
+ options.OptionIDs = optionIDs;
+
+ return options;
+ }
+}
diff --git a/Wonderking/Packets/Incoming/LoginInfoPacket.cs b/Wonderking/Packets/Incoming/LoginInfoPacket.cs
index 4758ef7..dd9ecdd 100644
--- a/Wonderking/Packets/Incoming/LoginInfoPacket.cs
+++ b/Wonderking/Packets/Incoming/LoginInfoPacket.cs
@@ -11,8 +11,9 @@ public class LoginInfoPacket : IPacket
public void Deserialize(byte[] data)
{
- this.Username = Encoding.ASCII.GetString(data, 0, 20).TrimEnd('\0');
- this.Password = Encoding.ASCII.GetString(data, 20, 31).TrimEnd('\0');
+ this.Username = Encoding.ASCII.GetString(data, 0, 20).TrimEnd('\0').TrimEnd('\n').TrimEnd('\0');
+ // Remove unnecessary Symbols
+ this.Password = Encoding.ASCII.GetString(data, 20, 31).TrimEnd('\0').TrimEnd('\n').TrimEnd('\0');
}
public byte[] Serialize()
diff --git a/Wonderking/Packets/OperationCode.cs b/Wonderking/Packets/OperationCode.cs
index f8b9eea..97e5584 100644
--- a/Wonderking/Packets/OperationCode.cs
+++ b/Wonderking/Packets/OperationCode.cs
@@ -4,5 +4,7 @@ public enum OperationCode : ushort
{
LoginInfo = 11,
LoginResponse = 12,
- ChannelSelection
+ ChannelSelection = 13,
+ ChannelSelectionResponse = 13,
+ CharacterSelectionSetGuildName = 19,
}
diff --git a/Wonderking/Packets/Outgoing/BaseStats.cs b/Wonderking/Packets/Outgoing/BaseStats.cs
new file mode 100644
index 0000000..b525c87
--- /dev/null
+++ b/Wonderking/Packets/Outgoing/BaseStats.cs
@@ -0,0 +1,14 @@
+using JetBrains.Annotations;
+
+namespace Wonderking.Packets.Outgoing;
+
+[UsedImplicitly]
+public class BaseStats
+{
+ public required short Strength { get; set; }
+ public required short Dexterity { get; set; }
+ public required short Intelligence { get; set; }
+ public required short Vitality { get; set; }
+ public required short Luck { get; set; }
+ public required short Wisdom { get; set; }
+}
diff --git a/Wonderking/Packets/Outgoing/ChannelSelectionResponsePacket.cs b/Wonderking/Packets/Outgoing/ChannelSelectionResponsePacket.cs
new file mode 100644
index 0000000..f803786
--- /dev/null
+++ b/Wonderking/Packets/Outgoing/ChannelSelectionResponsePacket.cs
@@ -0,0 +1,70 @@
+using System.Buffers.Binary;
+using System.Text;
+
+namespace Wonderking.Packets.Outgoing;
+
+[PacketId(OperationCode.ChannelSelectionResponse)]
+public class ChannelSelectionResponsePacket : IPacket
+{
+ public required byte ChannelIsFullFlag { get; set; }
+ public required string Endpoint { get; set; }
+ public required ushort Port { get; set; }
+ public required CharacterData[] Characters { get; set; }
+
+ public void Deserialize(byte[] data)
+ {
+ throw new NotSupportedException();
+ }
+
+ public byte[] Serialize()
+ {
+ Span data = stackalloc byte[1 + 16 + 2 + 1 + 132 * this.Characters.Length];
+ data.Clear();
+ data[0] = this.ChannelIsFullFlag;
+ Encoding.ASCII.GetBytes(this.Endpoint, data.Slice(1, 16));
+ BinaryPrimitives.WriteUInt16LittleEndian(data.Slice(17, 2), this.Port);
+ data[19] = (byte)this.Characters.Length;
+
+ // Character Data
+ for (var i = 0; i < Characters.Length; i++)
+ {
+ var character = Characters[i];
+ BinaryPrimitives.WriteInt32LittleEndian(data.Slice(20 + (i * 132), 4), i);
+ Encoding.ASCII.GetBytes(character.Name, data.Slice(24 + (i * 132), 20));
+
+ // Job Data
+ data[44 + (i * 132)] = character.Job.FirstJob;
+ data[45 + (i * 132)] = character.Job.SecondJob;
+ data[46 + (i * 132)] = character.Job.ThirdJob;
+ data[47 + (i * 132)] = character.Job.FourthJob;
+
+ data[48 + (i * 132)] = (byte)character.Gender;
+ BinaryPrimitives.WriteUInt16LittleEndian(data.Slice(49 + (i * 132), 2), character.Level);
+ data[51 + (i * 132)] = (byte)character.Experience;
+
+ // Stats
+ BinaryPrimitives.WriteInt16LittleEndian(data.Slice(52 + (i * 132), 2), character.Stats.Strength);
+ BinaryPrimitives.WriteInt16LittleEndian(data.Slice(54 + (i * 132), 2), character.Stats.Dexterity);
+ BinaryPrimitives.WriteInt16LittleEndian(data.Slice(56 + (i * 132), 2), character.Stats.Intelligence);
+ BinaryPrimitives.WriteInt16LittleEndian(data.Slice(58 + (i * 132), 2), character.Stats.Vitality);
+ BinaryPrimitives.WriteInt16LittleEndian(data.Slice(60 + (i * 132), 2), character.Stats.Luck);
+ BinaryPrimitives.WriteInt16LittleEndian(data.Slice(62 + (i * 132), 2), character.Stats.Wisdom);
+
+ BinaryPrimitives.WriteInt32LittleEndian(data.Slice(64 + (i * 132), 4), character.Health);
+ BinaryPrimitives.WriteInt32LittleEndian(data.Slice(68 + (i * 132), 4), character.Mana);
+
+ for (var j = 0; j < 20; j++)
+ {
+ // Equipped Items
+ BinaryPrimitives.WriteUInt16LittleEndian(data.Slice(72 + (i * 132) + (j * 2), 2),
+ character.EquippedItems.Length > j ? character.EquippedItems[j] : (ushort)0);
+
+ // Equipped Cash Items
+ BinaryPrimitives.WriteUInt16LittleEndian(data.Slice(112 + (i * 132) + (j * 2), 2),
+ character.EquippedCashItems.Length > j ? character.EquippedCashItems[j] : (ushort)0);
+ }
+ }
+
+ return data.ToArray();
+ }
+}
diff --git a/Wonderking/Packets/Outgoing/CharacterData.cs b/Wonderking/Packets/Outgoing/CharacterData.cs
new file mode 100644
index 0000000..b6d95af
--- /dev/null
+++ b/Wonderking/Packets/Outgoing/CharacterData.cs
@@ -0,0 +1,17 @@
+using Wonderking.Game.Data.Character;
+
+namespace Wonderking.Packets.Outgoing;
+
+public struct CharacterData
+{
+ public required string Name { get; set; }
+ public required JobData Job { get; set; }
+ public required Gender Gender { get; set; }
+ public required ushort Level { get; set; }
+ public required float Experience { get; set; }
+ public required BaseStats Stats { get; set; }
+ public required int Health { get; set; }
+ public required int Mana { get; set; }
+ public required ushort[] EquippedItems { get; set; }
+ public required ushort[] EquippedCashItems { get; set; }
+}
diff --git a/Wonderking/Packets/Outgoing/CharacterGuildInfo.cs b/Wonderking/Packets/Outgoing/CharacterGuildInfo.cs
new file mode 100644
index 0000000..7f41eb0
--- /dev/null
+++ b/Wonderking/Packets/Outgoing/CharacterGuildInfo.cs
@@ -0,0 +1,5 @@
+namespace Wonderking.Packets.Outgoing;
+
+public struct CharacterGuildInfo
+{
+}
diff --git a/Wonderking/Packets/Outgoing/CharacterSelectionSetGuildNamePacket.cs b/Wonderking/Packets/Outgoing/CharacterSelectionSetGuildNamePacket.cs
new file mode 100644
index 0000000..bc6bf2f
--- /dev/null
+++ b/Wonderking/Packets/Outgoing/CharacterSelectionSetGuildNamePacket.cs
@@ -0,0 +1,30 @@
+using System.Text;
+
+namespace Wonderking.Packets.Outgoing;
+
+[PacketId(OperationCode.CharacterSelectionSetGuildName)]
+public class CharacterSelectionSetGuildNamePacket : IPacket
+{
+ public required string[] GuildNames { get; set; }
+
+ public void Deserialize(byte[] data)
+ {
+ throw new NotSupportedException();
+ }
+
+ public byte[] Serialize()
+ {
+ Span data = stackalloc byte[1 + (16 + 1 + 1) * this.GuildNames.Length];
+ data.Clear();
+ data[0] = (byte)this.GuildNames.Length;
+ for (var i = 0; i < this.GuildNames.Length; i++)
+ {
+ data[1 + (i * (16 + 1 + 1))] = (byte)i;
+ Encoding.ASCII.GetBytes(this.GuildNames[i], data.Slice(2 + (i * (16 + 1 + 1)), 16));
+ // Null terminator
+ data[18 + (i * (16 + 1 + 1))] = 0;
+ }
+
+ return data.ToArray();
+ }
+}
diff --git a/Wonderking/Packets/Outgoing/JobData.cs b/Wonderking/Packets/Outgoing/JobData.cs
new file mode 100644
index 0000000..6c2c9fe
--- /dev/null
+++ b/Wonderking/Packets/Outgoing/JobData.cs
@@ -0,0 +1,12 @@
+using JetBrains.Annotations;
+
+namespace Wonderking.Packets.Outgoing;
+
+[UsedImplicitly]
+public class JobData
+{
+ public required byte FirstJob { get; set; }
+ public required byte SecondJob { get; set; }
+ public required byte ThirdJob { get; set; }
+ public required byte FourthJob { get; set; }
+}
diff --git a/Wonderking/Utils/ByteArrayConverter.cs b/Wonderking/Utils/ByteArrayConverter.cs
new file mode 100644
index 0000000..76259f5
--- /dev/null
+++ b/Wonderking/Utils/ByteArrayConverter.cs
@@ -0,0 +1,36 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Wonderking.Utils;
+
+public class ByteArrayConverter : JsonConverter
+{
+ public override byte[] Read(
+ ref Utf8JsonReader reader,
+ Type typeToConvert,
+ JsonSerializerOptions options)
+ {
+ var hexData = reader.GetString();
+ if (hexData != null)
+ {
+ return hexData.Split('-').Select(b => Convert.ToByte(b, 16)).ToArray();
+ }
+ throw new JsonException("Hex string is null.");
+ }
+
+ public override void Write(
+ Utf8JsonWriter writer,
+ byte[]? value,
+ JsonSerializerOptions options)
+ {
+ if (value == null)
+ {
+ writer.WriteNullValue();
+ }
+ else
+ {
+ var hexData = BitConverter.ToString(value).Replace("-", string.Empty);
+ writer.WriteStringValue(hexData);
+ }
+ }
+}
diff --git a/Wonderking/Wonderking.csproj b/Wonderking/Wonderking.csproj
index e8fbd2c..f41aaad 100644
--- a/Wonderking/Wonderking.csproj
+++ b/Wonderking/Wonderking.csproj
@@ -3,8 +3,9 @@
enable
enable
- net8.0;net7.0
strict
+ 12
+ net8.0
@@ -12,8 +13,8 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
@@ -30,5 +31,13 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
+
+ True
+ \
+
+
+
+
+
diff --git a/build-image.ps1 b/build-image.ps1
new file mode 100644
index 0000000..c46b322
--- /dev/null
+++ b/build-image.ps1
@@ -0,0 +1,2 @@
+#!sh
+docker build --platform linux/arm64,linux/amd64 -f Server/Dockerfile -t continuity .
diff --git a/global.json b/global.json
index 7fd6a2a..f7fb55b 100644
--- a/global.json
+++ b/global.json
@@ -1,7 +1,7 @@
{
"sdk": {
- "version": "7.0.404",
- "rollForward": "latestMajor",
- "allowPrerelease": true
+ "version": "8.0.100",
+ "rollForward": "latestMinor",
+ "allowPrerelease": false
}
-}
\ No newline at end of file
+}