Merge pull request 'feature/84-character-creation' (#88) from feature/84-character-creation into master
All checks were successful
Build documentation / preprocess (push) Successful in 2s
Build, Package and Push Images / preprocess (push) Successful in 2s
Build documentation / docs (push) Successful in 21s
Build, Package and Push Images / build (push) Successful in 24s
Build, Package and Push Images / sbom-scan (push) Successful in 41s
Build documentation / build-docs-container (push) Successful in 46s
Build documentation / deploy-wiki (push) Successful in 6s
Build, Package and Push Images / container-build (push) Successful in 1m47s
Build, Package and Push Images / sonarqube (push) Successful in 1m50s
Build, Package and Push Images / container-sbom-scan (push) Successful in 37s

Reviewed-on: #88
This commit is contained in:
rainote 2023-11-17 07:29:21 +00:00
commit 17a2a49cfd
61 changed files with 2807 additions and 559 deletions

124
.gitea/workflows/docs.yaml Normal file
View file

@ -0,0 +1,124 @@
name: Build documentation
run-name: ${{ gitea.actor }} is building the Wiki documentation
on:
push:
paths:
- Wiki/**
- Wiki.Dockerfile
env:
# Name of module and id separated by a slash
INSTANCE: Wiki/wiki
# Replace HI with the ID of the instance in capital letters
ARTIFACT: webHelpWIKI2-all.zip
# Writerside docker image version
DOCKER_VERSION: 232.10165.1
ALGOLIA_ARTIFACT: algolia-indexes-wiki.zip
jobs:
preprocess:
runs-on: ubuntu-latest
outputs:
sanitized_branch_name: ${{ steps.sanitize.outputs.sanitized_branch_name }}
steps:
- name: Sanitize branch name
id: sanitize
run: echo "::set-output name=sanitized_branch_name::$(echo ${{ github.ref_name }} | sed 's/\//-/g')"
docs:
runs-on: ubuntu-latest
container: registry.jetbrains.team/p/writerside/builder/writerside-builder:${{env.DOCKER_VERSION}}
steps:
- name: Install basic dependencies
run: |
wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash
echo "::add-path::$HOME/.nvm"
export NVM_DIR="$([ -z "${XDG_CONFIG_HOME-}" ] && printf %s "${HOME}/.nvm" || printf %s "${XDG_CONFIG_HOME}/nvm")"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
nvm install 18
nvm use 18
echo "::add-path::$(dirname $(which npm))"
nvm --version
- name: Check Node.js version
run: |
node -v
npm -v
- name: Checkout repository
uses: https://github.com/actions/checkout@v3
- name: Build docs
run: |
set -e
export DISPLAY=:99
Xvfb :99 &
/opt/builder/bin/idea.sh helpbuilderinspect -source-dir . -product ${{env.INSTANCE}} -output-dir artifacts/ || true
echo "Test existing of ${{ env.ARTIFACT }} artifact"
test -e artifacts/${{ env.ARTIFACT }}
- name: rename artifact
run: |
mv artifacts/${{ env.ARTIFACT }} artifacts/wiki.zip
- name: Upload documentation
uses: actions/upload-artifact@v3
with:
name: wiki.zip
path: artifacts/wiki.zip
retention-days: 14
- name: Upload algolia-indexes
uses: actions/upload-artifact@v3
with:
name: algolia-indexes.zip
path: artifacts/${{ env.ALGOLIA_ARTIFACT }}
retention-days: 14
build-docs-container:
runs-on: ubuntu-latest
container: catthehacker/ubuntu:act-latest
needs: [docs, preprocess]
steps:
- name: Checkout repository
uses: https://github.com/actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
registry: ${{ github.server_url }}
username: ${{ github.actor }}
password: ${{ secrets.REGISTRY_TOKEN }}
- name: Retrieve docs artifact
uses: actions/download-artifact@v3
with:
name: wiki.zip
path: ${{ github.workspace }}
- name: Unzip wiki.zip into .public
run: |
mkdir .public
unzip -jo -qq ./wiki.zip/wiki.zip -d .public
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
file: Wiki.Dockerfile
push: true
tags: forge.rainote.dev/${{ github.repository }}:${{ needs.preprocess.outputs.sanitized_branch_name }}-wiki
platforms: linux/amd64,linux/arm64
- name: Build and push to latest
if: github.ref_name == 'master'
uses: docker/build-push-action@v5
with:
context: .
file: Wiki.Dockerfile
push: true
tags: forge.rainote.dev/${{ github.repository }}:latest-wiki
platforms: linux/amd64, linux/arm64
deploy-wiki:
runs-on: ubuntu-latest
container: catthehacker/ubuntu:act-latest
needs: [build-docs-container, docs, preprocess]
steps:
- name: Deploy Image to CapRrover
run: |
docker run caprover/cli-caprover:2.2.3 caprover deploy --caproverUrl ${{ secrets.CAPROVER_SERVER }} --appToken ${{ secrets.WIKI_APP_TOKEN }} --imageName forge.rainote.dev/${{ github.repository }}:${{ needs.preprocess.outputs.sanitized_branch_name }}-wiki -a ${{ secrets.WIKI_APP_NAME }}

View file

@ -1,16 +1,11 @@
name: Build, Package and Push Images
name: Build, Package and Push Images
run-name: ${{ gitea.actor }} is building the Server application
on: [ push ]
env:
# Name of module and id separated by a slash
INSTANCE: Wiki/wiki
# Replace HI with the ID of the instance in capital letters
ARTIFACT: webHelpWIKI2-all.zip
# Writerside docker image version
DOCKER_VERSION: 232.10165.1
ALGOLIA_ARTIFACT: algolia-indexes-wiki.zip
on:
push:
paths-ignore:
- Wiki/**
- Benchmarks/**
- .run/**
jobs:
preprocess:
@ -22,56 +17,6 @@ jobs:
id: sanitize
run: echo "::set-output name=sanitized_branch_name::$(echo ${{ github.ref_name }} | sed 's/\//-/g')"
# docs:
# runs-on: ubuntu-latest
# container: registry.jetbrains.team/p/writerside/builder/writerside-builder:${{env.DOCKER_VERSION}}
# steps:
# - name: Install basic dependencies
# run: |
# wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash
# echo "::add-path::$HOME/.nvm"
# export PATH="$HOME/.nvm:$PATH"
# export NVM_DIR="$HOME/.nvm"
# [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
# echo '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"' >> ~/.bashrc
# echo "$HOME/.nvm" >> $GITHUB_PATH
# nvm --version
# - name: Install Node
# run: |
# echo $PATH
# echo $GITHUB_PATH
# cat $GITHUB_PATH
# echo $NVM_DIR
# ls -la $HOME/.nvm
# nvm install 18
# nvm use 18
# node -v
# - name: Checkout repository
# uses: https://github.com/actions/checkout@v3
# - name: Build docs
# run: |
# set -e
# export DISPLAY=:99
# Xvfb :99 &
# /opt/builder/bin/idea.sh helpbuilderinspect -source-dir . -product ${{env.INSTANCE}} -output-dir artifacts/ || true
# echo "Test existing of ${{ env.ARTIFACT }} artifact"
# test -e artifacts/${{ env.ARTIFACT }}
# - name: rename artifact
# run: |
# mv artifacts/${{ env.ARTIFACT }} artifacts/wiki.zip
# - name: Upload documentation
# uses: actions/upload-artifact@v3
# with:
# name: docs
# path: artifacts/wiki.zip
# retention-days: 14
# - name: Upload algolia-indexes
# uses: actions/upload-artifact@v3
# with:
# name: algolia-indexes
# path: artifacts/${{ env.ALGOLIA_ARTIFACT }}
# retention-days: 14
build:
runs-on: ubuntu-latest
steps:
@ -148,13 +93,13 @@ jobs:
projectName: ${{ secrets.DEPENDENCY_TRACK_PROJECT_NAME }}
autoCreate: true
# set projectversion to be the branch name
projectVersion: "${{ github.ref_name }}"
bomFilename: "${{ github.workspace }}/bom.xml"
projectVersion: ${{ github.ref_name }}
bomFilename: ${{ github.workspace }}/bom.xml
container-build:
runs-on: ubuntu-latest
container: catthehacker/ubuntu:act-latest
needs: [ build, preprocess ]
needs: [build, preprocess]
steps:
- uses: actions/checkout@v3
- name: Setup dotnet
@ -192,7 +137,7 @@ jobs:
platforms: linux/amd64, linux/arm64
container-sbom-scan:
needs: [ container-build, preprocess ]
needs: [container-build, preprocess]
runs-on: ubuntu-latest
container: catthehacker/ubuntu:act-latest
steps:
@ -226,9 +171,9 @@ jobs:
with:
apiKey: ${{ secrets.DEPENDENCY_TRACK_API_KEY }}
serverHostname: ${{ secrets.DEPENDENCY_TRACK_URL }}
projectName: "${{ secrets.DEPENDENCY_TRACK_PROJECT_NAME }}-container"
projectName: ${{ secrets.DEPENDENCY_TRACK_PROJECT_NAME }}-container
autoCreate: true
# set projectversion to be the branch name
projectVersion: "${{ github.ref_name }}"
bomFilename: "${{ github.workspace }}/container-bom.json"
projectVersion: ${{ github.ref_name }}
bomFilename: ${{ github.workspace }}/container-bom.json

View file

@ -1,9 +1,24 @@
repos:
- repo: local
repos:
- repo: local
hooks:
#Use dotnet format already installed on your machine
- id: dotnet-format
name: dotnet-format
language: system
entry: dotnet format --include
types_or: ["c#", "vb"]
types_or: [c#, vb]
- repo: https://github.com/Mateusz-Grzelinski/actionlint-py
rev: v1.6.26.11
hooks:
- id: actionlint
additional_dependencies: [pyflakes>=3.0.1, shellcheck-py>=0.9.0.5]
- repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks
rev: v2.11.0
hooks:
- id: pretty-format-yaml
args: [--autofix, --indent, '2']
- repo: https://github.com/hadolint/hadolint
rev: v2.12.0
hooks:
- id: hadolint-docker
args: [--ignore, SC2086]

View file

@ -1,3 +1,4 @@
using System.Net.Sockets;
using Wonderking.Packets;
namespace Server;
@ -31,7 +32,7 @@ public class AuthSession : TcpSession
public void Send(IPacket packet)
{
var type = packet.GetType();
this._logger.LogTrace("Packet of type {Type} is being serialized", type.Name);
this._logger.LogInformation("Packet of type {Type} is being serialized", type.Name);
var packetIdAttribute = type.GetCustomAttribute<PacketIdAttribute>();
if (packetIdAttribute == null)
{
@ -59,8 +60,8 @@ public class AuthSession : TcpSession
buffer[2 + i] = bytesOfOpcode[i];
}
this._logger.LogTrace("Packet data being parsed is: {Data}", BitConverter.ToString(packetData.ToArray()));
this._logger.LogTrace("Packet being parsed is: {Data}", BitConverter.ToString(buffer.ToArray()));
this._logger.LogInformation("Packet data being parsed is: {Data}", BitConverter.ToString(packetData.ToArray()));
this._logger.LogInformation("Packet being parsed is: {Data}", BitConverter.ToString(buffer.ToArray()));
this.Send(buffer);
}
@ -105,4 +106,9 @@ public class AuthSession : TcpSession
return buffer;
}
protected override void OnError(SocketError error)
{
_logger.LogWarning("An error has occured: {Error}", error);
}
}

View file

@ -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,18 @@ public class Account
this.Salt = salt;
}
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid Id { get; set; }
[Column(TypeName = "varchar(20)")]
[MaxLength(20)]
public string Username { get; set; }
public byte[] Password { get; set; }
public string Email { 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<Character> Characters { get; } = new List<Character>();
[Column(TypeName = "bytea")] public byte[] Salt { get; set; }
public virtual ICollection<Character> Characters { get; } = new List<Character>();
}

View file

@ -1,29 +1,38 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
using Wonderking.Game.Data.Character;
using Wonderking.Packets.Outgoing.Data;
namespace Server.DB.Documents;
[Index(nameof(Name), IsUnique = true), Index(nameof(Id), IsUnique = true)]
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; }
[Column(TypeName = "varchar(20)")]
[MaxLength(20)]
public string Name { get; set; }
public short LastXCoordinate { get; set; }
public short LastYCoordinate { get; set; }
public PvPLevel PvPLevel { get; set; }
public Gender Gender { get; set; }
public long Experience { get; set; }
public byte Level { get; set; }
public ICollection<InventoryItem> InventoryItems { get; set; }
public virtual ICollection<InventoryItem> 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; }
}

View file

@ -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<GuildMember> GuildMembers { get; set; }
public virtual ICollection<GuildMember> GuildMembers { get; set; }
}

View file

@ -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 virtual Character Character { get; set; }
public virtual Guild Guild { get; set; }
public GuildRank Rank { get; set; }
}

View file

@ -3,12 +3,12 @@ namespace Server.DB.Documents;
public class InventoryItem
{
public Guid CharacterId { get; set; }
public Character Character { get; set; }
public virtual 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 InventoryTab InventoryTab { get; set; }
public byte Level { get; set; }
public byte Rarity { get; set; }
public byte AddOption { get; set; }

View file

@ -1,6 +1,6 @@
namespace Server.DB.Documents;
public enum ItemType : byte
public enum InventoryTab : byte
{
WornEquipment = 0,
WornCashEquipment = 1,

View file

@ -0,0 +1,333 @@
// <auto-generated />
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
{
/// <inheritdoc />
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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Email")
.HasColumnType("text");
b.Property<byte[]>("Password")
.HasColumnType("bytea");
b.Property<byte>("PermissionLevel")
.HasColumnType("smallint");
b.Property<byte[]>("Salt")
.HasColumnType("bytea");
b.Property<string>("Username")
.HasColumnType("varchar(20)");
b.HasKey("Id");
b.HasIndex("Username")
.IsUnique();
b.ToTable("Accounts");
});
modelBuilder.Entity("Server.DB.Documents.Character", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("AccountId")
.HasColumnType("uuid");
b.Property<long>("Experience")
.HasColumnType("bigint");
b.Property<byte>("Gender")
.HasColumnType("smallint");
b.Property<Guid>("GuildId")
.HasColumnType("uuid");
b.Property<int>("Health")
.HasColumnType("integer");
b.Property<short>("LastXCoordinate")
.HasColumnType("smallint");
b.Property<short>("LastYCoordinate")
.HasColumnType("smallint");
b.Property<byte>("Level")
.HasColumnType("smallint");
b.Property<int>("Mana")
.HasColumnType("integer");
b.Property<int>("MapId")
.HasColumnType("integer");
b.Property<string>("Name")
.HasColumnType("varchar(20)");
b.Property<byte>("PvPLevel")
.HasColumnType("smallint");
b.Property<byte>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Name")
.HasColumnType("text");
b.Property<string>("Notice")
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Guild");
});
modelBuilder.Entity("Server.DB.Documents.GuildMember", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("CharacterId")
.HasColumnType("uuid");
b.Property<Guid>("GuildId")
.HasColumnType("uuid");
b.Property<byte>("Rank")
.HasColumnType("smallint");
b.HasKey("Id");
b.HasIndex("CharacterId");
b.HasIndex("GuildId");
b.ToTable("GuildMember");
});
modelBuilder.Entity("Server.DB.Documents.InventoryItem", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<byte>("AddOption")
.HasColumnType("smallint");
b.Property<byte>("AddOption2")
.HasColumnType("smallint");
b.Property<byte>("AddOption3")
.HasColumnType("smallint");
b.Property<Guid>("CharacterId")
.HasColumnType("uuid");
b.Property<int>("Count")
.HasColumnType("integer");
b.Property<byte>("InventoryTab")
.HasColumnType("smallint");
b.Property<int>("ItemId")
.HasColumnType("integer");
b.Property<byte>("Level")
.HasColumnType("smallint");
b.Property<short>("Option")
.HasColumnType("smallint");
b.Property<short>("Option2")
.HasColumnType("smallint");
b.Property<short>("Option3")
.HasColumnType("smallint");
b.Property<byte>("Rarity")
.HasColumnType("smallint");
b.Property<byte>("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<Guid>("CharacterId")
.HasColumnType("uuid");
b1.Property<short>("Dexterity")
.HasColumnType("smallint");
b1.Property<short>("Intelligence")
.HasColumnType("smallint");
b1.Property<short>("Luck")
.HasColumnType("smallint");
b1.Property<short>("Strength")
.HasColumnType("smallint");
b1.Property<short>("Vitality")
.HasColumnType("smallint");
b1.Property<short>("Wisdom")
.HasColumnType("smallint");
b1.HasKey("CharacterId");
b1.ToTable("Characters");
b1.WithOwner()
.HasForeignKey("CharacterId");
});
b.OwnsOne("Wonderking.Packets.Outgoing.Data.JobData", "JobData", b1 =>
{
b1.Property<Guid>("CharacterId")
.HasColumnType("uuid");
b1.Property<byte>("FirstJob")
.HasColumnType("smallint");
b1.Property<byte>("FourthJob")
.HasColumnType("smallint");
b1.Property<byte>("SecondJob")
.HasColumnType("smallint");
b1.Property<byte>("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
}
}
}

View file

@ -0,0 +1,27 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Server.DB.Migrations;
/// <inheritdoc />
public partial class GuildIsNotRequired : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "ItemType",
table: "InventoryItem",
newName: "InventoryTab");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "InventoryTab",
table: "InventoryItem",
newName: "ItemType");
}
}

View file

@ -0,0 +1,322 @@
// <auto-generated />
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
{
/// <inheritdoc />
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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Email")
.HasColumnType("text");
b.Property<byte[]>("Password")
.HasColumnType("bytea");
b.Property<byte>("PermissionLevel")
.HasColumnType("smallint");
b.Property<byte[]>("Salt")
.HasColumnType("bytea");
b.Property<string>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid?>("AccountId")
.HasColumnType("uuid");
b.Property<long>("Experience")
.HasColumnType("bigint");
b.Property<byte>("Gender")
.HasColumnType("smallint");
b.Property<Guid?>("GuildId")
.HasColumnType("uuid");
b.Property<int>("Health")
.HasColumnType("integer");
b.Property<short>("LastXCoordinate")
.HasColumnType("smallint");
b.Property<short>("LastYCoordinate")
.HasColumnType("smallint");
b.Property<byte>("Level")
.HasColumnType("smallint");
b.Property<int>("Mana")
.HasColumnType("integer");
b.Property<int>("MapId")
.HasColumnType("integer");
b.Property<string>("Name")
.HasColumnType("text");
b.Property<byte>("PvPLevel")
.HasColumnType("smallint");
b.HasKey("Id");
b.HasIndex("AccountId");
b.HasIndex("GuildId");
b.ToTable("Characters");
});
modelBuilder.Entity("Server.DB.Documents.Guild", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Name")
.HasColumnType("text");
b.Property<string>("Notice")
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Guild");
});
modelBuilder.Entity("Server.DB.Documents.GuildMember", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid?>("CharacterId")
.HasColumnType("uuid");
b.Property<Guid?>("GuildId")
.HasColumnType("uuid");
b.Property<byte>("Rank")
.HasColumnType("smallint");
b.HasKey("Id");
b.HasIndex("CharacterId");
b.HasIndex("GuildId");
b.ToTable("GuildMember");
});
modelBuilder.Entity("Server.DB.Documents.InventoryItem", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<byte>("AddOption")
.HasColumnType("smallint");
b.Property<byte>("AddOption2")
.HasColumnType("smallint");
b.Property<byte>("AddOption3")
.HasColumnType("smallint");
b.Property<Guid>("CharacterId")
.HasColumnType("uuid");
b.Property<int>("Count")
.HasColumnType("integer");
b.Property<byte>("InventoryTab")
.HasColumnType("smallint");
b.Property<int>("ItemId")
.HasColumnType("integer");
b.Property<byte>("Level")
.HasColumnType("smallint");
b.Property<short>("Option")
.HasColumnType("smallint");
b.Property<short>("Option2")
.HasColumnType("smallint");
b.Property<short>("Option3")
.HasColumnType("smallint");
b.Property<byte>("Rarity")
.HasColumnType("smallint");
b.Property<byte>("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<Guid>("CharacterId")
.HasColumnType("uuid");
b1.Property<short>("Dexterity")
.HasColumnType("smallint");
b1.Property<short>("Intelligence")
.HasColumnType("smallint");
b1.Property<short>("Luck")
.HasColumnType("smallint");
b1.Property<short>("Strength")
.HasColumnType("smallint");
b1.Property<short>("Vitality")
.HasColumnType("smallint");
b1.Property<short>("Wisdom")
.HasColumnType("smallint");
b1.HasKey("CharacterId");
b1.ToTable("Characters");
b1.WithOwner()
.HasForeignKey("CharacterId");
});
b.OwnsOne("Wonderking.Packets.Outgoing.Data.JobData", "JobData", b1 =>
{
b1.Property<Guid>("CharacterId")
.HasColumnType("uuid");
b1.Property<byte>("FirstJob")
.HasColumnType("smallint");
b1.Property<byte>("FourthJob")
.HasColumnType("smallint");
b1.Property<byte>("SecondJob")
.HasColumnType("smallint");
b1.Property<byte>("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
}
}
}

View file

@ -0,0 +1,230 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Server.DB.Migrations;
/// <inheritdoc />
public partial class SwitchToDataAnnotations : Migration
{
/// <inheritdoc />
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<Guid>(
name: "GuildId",
table: "GuildMember",
type: "uuid",
nullable: true,
oldClrType: typeof(Guid),
oldType: "uuid");
migrationBuilder.AlterColumn<Guid>(
name: "CharacterId",
table: "GuildMember",
type: "uuid",
nullable: true,
oldClrType: typeof(Guid),
oldType: "uuid");
migrationBuilder.AlterColumn<string>(
name: "Name",
table: "Characters",
type: "text",
nullable: true,
oldClrType: typeof(string),
oldType: "varchar(20)",
oldNullable: true);
migrationBuilder.AlterColumn<Guid>(
name: "GuildId",
table: "Characters",
type: "uuid",
nullable: true,
oldClrType: typeof(Guid),
oldType: "uuid");
migrationBuilder.AlterColumn<Guid>(
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");
}
/// <inheritdoc />
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<Guid>(
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<Guid>(
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<string>(
name: "Name",
table: "Characters",
type: "varchar(20)",
nullable: true,
oldClrType: typeof(string),
oldType: "text",
oldNullable: true);
migrationBuilder.AlterColumn<Guid>(
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<Guid>(
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<byte>(
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);
}
}

View file

@ -0,0 +1,333 @@
// <auto-generated />
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("20231116110504_DBPoolingAndLazyLoadingSupport")]
partial class DBPoolingAndLazyLoadingSupport
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.0")
.HasAnnotation("Proxies:ChangeTracking", false)
.HasAnnotation("Proxies:CheckEquality", false)
.HasAnnotation("Proxies:LazyLoading", true)
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Server.DB.Documents.Account", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Email")
.HasColumnType("text");
b.Property<byte[]>("Password")
.HasColumnType("bytea");
b.Property<byte>("PermissionLevel")
.HasColumnType("smallint");
b.Property<byte[]>("Salt")
.HasColumnType("bytea");
b.Property<string>("Username")
.HasMaxLength(20)
.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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid?>("AccountId")
.HasColumnType("uuid");
b.Property<long>("Experience")
.HasColumnType("bigint");
b.Property<byte>("Gender")
.HasColumnType("smallint");
b.Property<Guid?>("GuildId")
.HasColumnType("uuid");
b.Property<int>("Health")
.HasColumnType("integer");
b.Property<short>("LastXCoordinate")
.HasColumnType("smallint");
b.Property<short>("LastYCoordinate")
.HasColumnType("smallint");
b.Property<byte>("Level")
.HasColumnType("smallint");
b.Property<int>("Mana")
.HasColumnType("integer");
b.Property<int>("MapId")
.HasColumnType("integer");
b.Property<string>("Name")
.HasMaxLength(20)
.HasColumnType("varchar(20)");
b.Property<byte>("PvPLevel")
.HasColumnType("smallint");
b.HasKey("Id");
b.HasIndex("AccountId");
b.HasIndex("GuildId");
b.HasIndex("Id")
.IsUnique();
b.HasIndex("Name")
.IsUnique();
b.ToTable("Characters");
});
modelBuilder.Entity("Server.DB.Documents.Guild", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Name")
.HasColumnType("text");
b.Property<string>("Notice")
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Guild");
});
modelBuilder.Entity("Server.DB.Documents.GuildMember", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid?>("CharacterId")
.HasColumnType("uuid");
b.Property<Guid?>("GuildId")
.HasColumnType("uuid");
b.Property<byte>("Rank")
.HasColumnType("smallint");
b.HasKey("Id");
b.HasIndex("CharacterId");
b.HasIndex("GuildId");
b.ToTable("GuildMember");
});
modelBuilder.Entity("Server.DB.Documents.InventoryItem", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<byte>("AddOption")
.HasColumnType("smallint");
b.Property<byte>("AddOption2")
.HasColumnType("smallint");
b.Property<byte>("AddOption3")
.HasColumnType("smallint");
b.Property<Guid>("CharacterId")
.HasColumnType("uuid");
b.Property<int>("Count")
.HasColumnType("integer");
b.Property<byte>("InventoryTab")
.HasColumnType("smallint");
b.Property<int>("ItemId")
.HasColumnType("integer");
b.Property<byte>("Level")
.HasColumnType("smallint");
b.Property<short>("Option")
.HasColumnType("smallint");
b.Property<short>("Option2")
.HasColumnType("smallint");
b.Property<short>("Option3")
.HasColumnType("smallint");
b.Property<byte>("Rarity")
.HasColumnType("smallint");
b.Property<byte>("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<Guid>("CharacterId")
.HasColumnType("uuid");
b1.Property<short>("Dexterity")
.HasColumnType("smallint");
b1.Property<short>("Intelligence")
.HasColumnType("smallint");
b1.Property<short>("Luck")
.HasColumnType("smallint");
b1.Property<short>("Strength")
.HasColumnType("smallint");
b1.Property<short>("Vitality")
.HasColumnType("smallint");
b1.Property<short>("Wisdom")
.HasColumnType("smallint");
b1.HasKey("CharacterId");
b1.ToTable("Characters");
b1.WithOwner()
.HasForeignKey("CharacterId");
});
b.OwnsOne("Wonderking.Packets.Outgoing.Data.JobData", "JobData", b1 =>
{
b1.Property<Guid>("CharacterId")
.HasColumnType("uuid");
b1.Property<byte>("FirstJob")
.HasColumnType("smallint");
b1.Property<byte>("FourthJob")
.HasColumnType("smallint");
b1.Property<byte>("SecondJob")
.HasColumnType("smallint");
b1.Property<byte>("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
}
}
}

View file

@ -0,0 +1,57 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Server.DB.Migrations;
/// <inheritdoc />
public partial class DBPoolingAndLazyLoadingSupport : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "Name",
table: "Characters",
type: "varchar(20)",
maxLength: 20,
nullable: true,
oldClrType: typeof(string),
oldType: "text",
oldNullable: true);
migrationBuilder.CreateIndex(
name: "IX_Characters_Id",
table: "Characters",
column: "Id",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Characters_Name",
table: "Characters",
column: "Name",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_Characters_Id",
table: "Characters");
migrationBuilder.DropIndex(
name: "IX_Characters_Name",
table: "Characters");
migrationBuilder.AlterColumn<string>(
name: "Name",
table: "Characters",
type: "text",
nullable: true,
oldClrType: typeof(string),
oldType: "varchar(20)",
oldMaxLength: 20,
oldNullable: true);
}
}

View file

@ -18,6 +18,9 @@ namespace Server.DB.Migrations
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.0")
.HasAnnotation("Proxies:ChangeTracking", false)
.HasAnnotation("Proxies:CheckEquality", false)
.HasAnnotation("Proxies:LazyLoading", true)
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
@ -41,10 +44,14 @@ namespace Server.DB.Migrations
.HasColumnType("bytea");
b.Property<string>("Username")
.HasMaxLength(20)
.HasColumnType("varchar(20)");
b.HasKey("Id");
b.HasIndex("Id")
.IsUnique();
b.HasIndex("Username")
.IsUnique();
@ -57,7 +64,7 @@ namespace Server.DB.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("AccountId")
b.Property<Guid?>("AccountId")
.HasColumnType("uuid");
b.Property<long>("Experience")
@ -66,7 +73,7 @@ namespace Server.DB.Migrations
b.Property<byte>("Gender")
.HasColumnType("smallint");
b.Property<Guid>("GuildId")
b.Property<Guid?>("GuildId")
.HasColumnType("uuid");
b.Property<int>("Health")
@ -88,20 +95,21 @@ namespace Server.DB.Migrations
.HasColumnType("integer");
b.Property<string>("Name")
.HasMaxLength(20)
.HasColumnType("varchar(20)");
b.Property<byte>("PvPLevel")
.HasColumnType("smallint");
b.Property<byte>("ServerId")
.HasColumnType("smallint");
b.HasKey("Id");
b.HasIndex("AccountId");
b.HasIndex("GuildId");
b.HasIndex("Id")
.IsUnique();
b.HasIndex("Name")
.IsUnique();
@ -131,10 +139,10 @@ namespace Server.DB.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("CharacterId")
b.Property<Guid?>("CharacterId")
.HasColumnType("uuid");
b.Property<Guid>("GuildId")
b.Property<Guid?>("GuildId")
.HasColumnType("uuid");
b.Property<byte>("Rank")
@ -170,12 +178,12 @@ namespace Server.DB.Migrations
b.Property<int>("Count")
.HasColumnType("integer");
b.Property<byte>("InventoryTab")
.HasColumnType("smallint");
b.Property<int>("ItemId")
.HasColumnType("integer");
b.Property<byte>("ItemType")
.HasColumnType("smallint");
b.Property<byte>("Level")
.HasColumnType("smallint");
@ -205,15 +213,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 +288,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");

View file

@ -1,53 +1,16 @@
using JetBrains.Annotations;
namespace Server.DB;
using Documents;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
public class WonderkingContext : DbContext
{
private readonly IConfiguration _configuration;
private readonly ILoggerFactory _loggerFactory;
public WonderkingContext(ILoggerFactory loggerFactory, IConfiguration configuration)
public WonderkingContext([NotNull] DbContextOptions options) : base(options)
{
this._loggerFactory = loggerFactory;
this._configuration = configuration;
}
public DbSet<Account> Accounts { get; set; }
public DbSet<Character> Characters { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) =>
optionsBuilder
.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<Account>(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<Character>(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<InventoryItem>(builder => { builder.HasKey(i => i.Id); }).Entity<Guild>(builder =>
{
builder.HasKey(g => g.Id);
builder.HasMany(g => g.GuildMembers).WithOne(g => g.Guild).HasForeignKey(g => g.GuildId)
.IsRequired();
});
}

View file

@ -7,17 +7,16 @@ ENV TZ=Etc/UTC
ENV DOTNET_TieredPGO=1
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
RUN echo "Target: $TARGETARCH"
RUN echo "Build: $BUILDPLATFORM"
RUN echo "Target: $TARGETARCH" && echo "Build: $BUILDPLATFORM"
WORKDIR /src
COPY ["Wonderking/Wonderking.csproj", "Wonderking/"]
COPY ["Server/Server.csproj", "Server/"]
RUN dotnet restore "Wonderking/Wonderking.csproj" -a $TARGETARCH
RUN dotnet restore "Server/Server.csproj" -a $TARGETARCH
RUN dotnet restore "Wonderking/Wonderking.csproj" -a $TARGETARCH && dotnet restore "Server/Server.csproj" -a $TARGETARCH
COPY . .
FROM build AS publish
RUN dotnet publish "Server/Server.csproj" -c Release -a $TARGETARCH --no-restore -f net8.0 -o /app
COPY ../config /app/config
FROM base AS final
WORKDIR /app

View file

@ -1,6 +1,5 @@
using Microsoft.EntityFrameworkCore;
using Server.DB.Documents;
using Wonderking.Game.Data.Character;
using Wonderking.Packets.Incoming;
using Wonderking.Packets.Outgoing;
using Wonderking.Packets.Outgoing.Data;
@ -26,27 +25,23 @@ public class ChannelSelectionHandler : IPacketHandler<ChannelSelectionPacket>
this._wonderkingContext = wonderkingContext;
}
public ChannelSelectionHandler()
{
}
public Task HandleAsync(ChannelSelectionPacket packet, TcpSession session)
public async Task HandleAsync(ChannelSelectionPacket packet, TcpSession session)
{
var authSession = (AuthSession)session;
ChannelSelectionResponsePacket responsePacket;
CharacterSelectionSetGuildNamePacket guildNameResponsePacket;
var guildNameResponsePacket = new CharacterSelectionSetGuildNamePacket { GuildNames = Array.Empty<string>() };
var hasCharacters = this._wonderkingContext.Accounts.Include(account => account.Characters)
.FirstOrDefault(a => a.Id == authSession.AccountId)?.Characters.Count > 0;
var testingChars = false;
if (hasCharacters && !testingChars)
var account = await this._wonderkingContext.Accounts
.FirstOrDefaultAsync(a => a.Id == authSession.AccountId).ConfigureAwait(true);
if (account != null && account.Characters.Count > 0)
{
responsePacket = new ChannelSelectionResponsePacket
{
ChannelIsFullFlag = 0,
Endpoint = "127.0.0.1",
Port = 12345,
Characters = this._wonderkingContext.Characters.Where(c => c.AccountId == authSession.AccountId)
Characters = await _wonderkingContext.Characters.AsNoTracking()
.Where(c => c.Account.Id == authSession.AccountId)
.Select(c =>
new CharacterData
{
@ -54,127 +49,43 @@ public class ChannelSelectionHandler : IPacketHandler<ChannelSelectionPacket>
Job = c.JobData,
Gender = c.Gender,
Level = c.Level,
Experience = 0,
// TODO: Calculate instead of clamping based on max experience for level
Experience = Math.Clamp(c.Experience, 0, 100),
Stats = c.BaseStats,
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(),
.ToArray()
})
.ToArray(),
.ToArrayAsync().ConfigureAwait(true),
};
guildNameResponsePacket = new CharacterSelectionSetGuildNamePacket
{
GuildNames = this._wonderkingContext.Characters.Where(c => c.AccountId == authSession.AccountId)
.Select(character => character.Guild.Name).ToArray()
};
guildNameResponsePacket.GuildNames = await _wonderkingContext.Characters
.Where(c => c.Account.Id == authSession.AccountId)
.Select(character => character.Guild.Name).ToArrayAsync().ConfigureAwait(true);
}
else
{
responsePacket = testingChars
? CreateTestChannelSelectionResponsePacket()
: new ChannelSelectionResponsePacket
responsePacket = new ChannelSelectionResponsePacket
{
ChannelIsFullFlag = 0,
Endpoint = "127.0.0.1",
Port = 12345,
Port = 2000,
Characters = Array.Empty<CharacterData>()
};
guildNameResponsePacket = new CharacterSelectionSetGuildNamePacket
{
GuildNames = new[] { "ABCDEFGHIJKLMNOP", "QRSTUVWXYZ123456", "A Guild Name For" }
};
}
authSession.Send(responsePacket);
if (guildNameResponsePacket.GuildNames.Length > 0)
if (guildNameResponsePacket.GuildNames.Length > 0 &&
guildNameResponsePacket.GuildNames.Select(n => n != string.Empty).Any())
{
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()
}
},
};
}
}

View file

@ -0,0 +1,129 @@
using Microsoft.EntityFrameworkCore;
using NetCoreServer;
using Server.DB;
using Server.DB.Documents;
using Server.Services;
using Wonderking.Game.Data.Character;
using Wonderking.Game.Mapping;
using Wonderking.Packets.Incoming;
using Wonderking.Packets.Outgoing;
using Wonderking.Packets.Outgoing.Data;
namespace Server.PacketHandlers;
public class CharacterCreationHandler : IPacketHandler<CharacterCreationPacket>
{
private readonly WonderkingContext _wonderkingContext;
private readonly ItemObjectPoolService _itemObjectPoolService;
private readonly CharacterStatsMappingConfiguration _characterStatsMapping;
public CharacterCreationHandler(WonderkingContext wonderkingContext, ItemObjectPoolService itemObjectPoolService,
CharacterStatsMappingConfiguration characterStatsMappingConfiguration)
{
_wonderkingContext = wonderkingContext;
_itemObjectPoolService = itemObjectPoolService;
_characterStatsMapping = characterStatsMappingConfiguration;
}
public async Task HandleAsync(CharacterCreationPacket packet, TcpSession session)
{
var authSession = session as AuthSession;
var account =
_wonderkingContext.Accounts.FirstOrDefault(a => authSession != null && a.Id == authSession.AccountId);
var mappedDefaultItems = _characterStatsMapping.DefaultCharacterMapping.Items
.Select(i => _itemObjectPoolService.GetBaseInventoryItem(i.Id, i.Quantity)).ToArray();
var firstJobConfig = packet.FirstJob switch
{
1 => _characterStatsMapping.Swordsman,
2 => _characterStatsMapping.Mage,
3 => _characterStatsMapping.Thief,
4 => _characterStatsMapping.Scout,
_ => _characterStatsMapping.Swordsman
};
var mappedJobItems = firstJobConfig.Items
.Select(i => _itemObjectPoolService.GetBaseInventoryItem(i.Id, i.Quantity)).ToArray();
InventoryItem[] items =
[
.. mappedDefaultItems,
.. mappedJobItems,
_itemObjectPoolService.GetBaseInventoryItem((ushort)((packet.FirstJob - 1) * 6 +
((byte)packet.Gender - 1) * 3 +
packet.Hair + 1)),
_itemObjectPoolService.GetBaseInventoryItem((ushort)((packet.FirstJob - 1) * 6 +
((byte)packet.Gender - 1) * 3 +
packet.Eyes + 25)),
_itemObjectPoolService.GetBaseInventoryItem((ushort)(((byte)packet.Gender - 1) * 3 +
packet.Shirt + 49)),
_itemObjectPoolService.GetBaseInventoryItem((ushort)(((byte)packet.Gender - 1) * 3 +
packet.Pants + 58)),
];
var calculateCurrentMana = CalculateCurrentMana(1, firstJobConfig);
var calculateCurrentHealth = CalculateCurrentHealth(1, firstJobConfig);
var toBeAddedCharacter = new Character
{
Account = account,
MapId = 300,
Name = packet.Name,
LastXCoordinate = 113,
LastYCoordinate = 0,
PvPLevel = PvPLevel.None,
Gender = packet.Gender,
Experience = 0,
Level = 1,
InventoryItems = items,
BaseStats = firstJobConfig.BaseStats,
JobData = new JobData { FirstJob = packet.FirstJob, SecondJob = 0, ThirdJob = 0, FourthJob = 0 },
Health = calculateCurrentHealth,
Mana = calculateCurrentMana
};
account?.Characters.Add(toBeAddedCharacter);
await _wonderkingContext.SaveChangesAsync().ConfigureAwait(true);
var amountOfCharacters = await _wonderkingContext.Characters.AsNoTrackingWithIdentityResolution()
.CountAsync(c => authSession != null && c.Account.Id == authSession.AccountId).ConfigureAwait(true);
var character = await _wonderkingContext.Characters.AsNoTrackingWithIdentityResolution()
.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(),
}).FirstAsync().ConfigureAwait(true);
authSession?.Send(new CharacterCreationResponsePacket
{
Character = character,
Slot = amountOfCharacters - 1,
isDuplicate = false,
});
}
private static int CalculateCurrentHealth(ushort level, JobSpecificMapping firstJobConfig)
{
return (int)((level - 1) * firstJobConfig.DynamicStats.HealthPerLevel +
firstJobConfig.BaseStats.Vitality * firstJobConfig.DynamicStats.HealthPerVitality);
}
private static int CalculateCurrentMana(ushort level, JobSpecificMapping firstJobConfig)
{
return (int)((level - 1) * firstJobConfig.DynamicStats.ManaPerLevel +
firstJobConfig.BaseStats.Wisdom * firstJobConfig.DynamicStats.ManaPerWisdom);
}
}

View file

@ -0,0 +1,42 @@
using Microsoft.EntityFrameworkCore;
using NetCoreServer;
using Server.DB;
using Wonderking.Packets.Incoming;
using Wonderking.Packets.Outgoing;
namespace Server.PacketHandlers;
public class CharacterDeletionHandler : IPacketHandler<CharacterDeletePacket>
{
private readonly WonderkingContext _wonderkingContext;
public CharacterDeletionHandler(WonderkingContext wonderkingContext)
{
_wonderkingContext = wonderkingContext;
}
public async Task HandleAsync(CharacterDeletePacket packet, TcpSession session)
{
using var authSession = session as AuthSession;
if (authSession == null)
{
session.Disconnect();
return;
}
var character = await _wonderkingContext.Characters.FirstOrDefaultAsync(x => x.Name == packet.Name &&
x.Account.Id == authSession.AccountId)
.ConfigureAwait(true);
var response = new CharacterDeleteResponsePacket { IsDeleted = 0 };
if (character == null)
{
authSession.Send(response);
return;
}
_wonderkingContext.Characters.Remove(character);
await _wonderkingContext.SaveChangesAsync().ConfigureAwait(false);
authSession.Send(response);
}
}

View file

@ -0,0 +1,10 @@
using System.Runtime.InteropServices;
namespace Server.PacketHandlers;
[StructLayout(LayoutKind.Auto)]
public struct CharacterMappingItemEntry
{
public required ushort Id { get; set; }
public required ushort Quantity { get; set; }
}

View file

@ -0,0 +1,25 @@
using NetCoreServer;
using Server.DB;
using Wonderking.Packets.Incoming;
using Wonderking.Packets.Outgoing;
namespace Server.PacketHandlers;
public class CharacterNameCheckHandler : IPacketHandler<CharacterNameCheckPacket>
{
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;
}
}

View file

@ -108,7 +108,7 @@ public class LoginHandler : IPacketHandler<LoginInfoPacket>
var loginResponsePacket = new LoginResponsePacket
{
ResponseReason = loginResponseReason,
ChannelData = new[] { new ServerChannelData { ChannelId = 0, LoadPercentage = 75, ServerId = 0 } },
ChannelData = new[] { new ServerChannelData { ChannelId = 0, LoadPercentage = 0, ServerId = 0 } },
UnknownFlag = 1,
IsGameMaster = true
};

View file

@ -1,5 +1,6 @@
using System.Net;
using System.Reflection;
using System.Text.Json;
using MassTransit;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
@ -8,23 +9,42 @@ using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Server.DB;
using Server.Services;
using Wonderking.Game.Mapping;
var builder = Host.CreateApplicationBuilder();
#if DEBUG
builder.Environment.EnvironmentName = "Development";
#endif
builder.Configuration.AddJsonFile("settings.json", true, true)
.AddJsonFile($"settings.{builder.Environment.EnvironmentName}.json", true)
.AddEnvironmentVariables().Build();
builder.Services.AddSingleton<CharacterStatsMappingConfiguration>(
JsonSerializer.Deserialize<CharacterStatsMappingConfiguration>(
File.ReadAllText("config/character-stats.mapping.json")) ?? throw new InvalidOperationException());
builder.Services.AddLogging();
builder.Logging.AddFile("Logs/Server-{Date}.log", LogLevel.Trace);
builder.Logging.AddFile("Logs/Server-{Date}.json.log", LogLevel.Trace, isJson: true);
builder.Services.AddEntityFrameworkNpgsql();
builder.Services.AddDbContext<WonderkingContext>();
var loggerFactory = LoggerFactory.Create(loggingBuilder =>
{
loggingBuilder.AddFile("logs/Server-{Date}.log", LogLevel.Trace);
loggingBuilder.AddFile("logs/Server-{Date}.json.log", LogLevel.Trace, isJson: true);
loggingBuilder.AddConsole();
});
builder.Services.AddDbContextPool<WonderkingContext>(o =>
{
using var configuration = builder.Configuration;
o.UseNpgsql(
$"Host={configuration["DB:Host"]};Username={configuration["DB:Username"]};Password={configuration["DB:Password"]};Database={configuration["DB:Database"]};Port={configuration["DB:Port"]}")
.EnableSensitiveDataLogging().UseLazyLoadingProxies().UseLoggerFactory(loggerFactory);
});
builder.Services.AddSingleton<ILoggerFactory>(loggerFactory);
builder.Services.AddSingleton<PacketDistributorService>();
builder.Services.AddSingleton<ItemObjectPoolService>();
builder.Services.AddHostedService<ItemObjectPoolService>();
builder.Services.AddHostedService(provider =>
provider.GetService<PacketDistributorService>() ?? throw new InvalidOperationException());
builder.Services.AddSingleton<ItemObjectPoolService>();
builder.Services.AddMassTransit(x =>
{
x.UsingInMemory((context, configurator) => configurator.ConfigureEndpoints(context));

View file

@ -66,6 +66,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Abstractions" Version="8.0.0"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Analyzers" Version="8.0.0"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View file

@ -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;
@ -8,35 +10,48 @@ namespace Server.Services;
public class ItemObjectPoolService : IHostedService
{
readonly ConcurrentDictionary<uint, ItemObject> _itemObjectPool = new();
readonly ConcurrentDictionary<uint, ItemObject> _itemObjectPool;
private readonly ItemReader _itemReader;
private readonly ILogger<ItemObjectPoolService> _logger;
public ItemObjectPoolService(IConfiguration configuration)
public ItemObjectPoolService(IConfiguration configuration, ILogger<ItemObjectPoolService> logger)
{
_logger = logger;
_itemReader = new ItemReader(configuration.GetSection("Game").GetSection("Data").GetValue<string>("Path") ??
string.Empty);
_itemObjectPool = new ConcurrentDictionary<uint, ItemObject>();
}
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 KeyNotFoundException($"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 +63,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
};
}
}

View file

@ -22,17 +22,22 @@ public class PacketDistributorService : IHostedService
{
private readonly ConcurrentQueue<RawPacket> _concurrentQueue;
private readonly
ImmutableDictionary<OperationCode,
private ImmutableDictionary<OperationCode,
Func<byte[], IPacket>> _deserializationMap;
private readonly ILogger<PacketDistributorService> _logger;
private readonly ConcurrentDictionary<OperationCode, object> _packetHandlersInstantiation;
private readonly IServiceProvider _serviceProvider;
private ConcurrentDictionary<OperationCode, object> _packetHandlersInstantiation;
public PacketDistributorService(ILogger<PacketDistributorService> logger, IServiceProvider serviceProvider)
{
this._concurrentQueue = new ConcurrentQueue<RawPacket>();
this._logger = logger;
_serviceProvider = serviceProvider;
}
public Task StartAsync(CancellationToken cancellationToken)
{
var tempDeserializationMap =
new Dictionary<OperationCode, Func<byte[], IPacket>>();
@ -43,7 +48,7 @@ public class PacketDistributorService : IHostedService
packetHandlers.ForEach(x =>
{
var packetHandler =
ActivatorUtilities.GetServiceOrCreateInstance(serviceProvider,
ActivatorUtilities.GetServiceOrCreateInstance(_serviceProvider,
x.Value);
this._packetHandlersInstantiation.TryAdd(x.Key, packetHandler);
});
@ -60,15 +65,14 @@ public class PacketDistributorService : IHostedService
Return(packetVariable);
}).Compile();
logger.PacketCreationFunctionCreated(packetsType.Key);
_logger.PacketCreationFunctionCreated(packetsType.Key);
tempDeserializationMap.Add(packetsType.Key, lambda);
}
this._deserializationMap = tempDeserializationMap.ToImmutableDictionary();
return Task.CompletedTask;
}
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
private Dictionary<OperationCode, Type> GetPacketsWithId(Assembly executingAssembly)

View file

@ -58,5 +58,17 @@ public class WonderkingAuthServer : TcpServer, IHostedService
base.OnStopped();
}
protected override void OnConnected(TcpSession session)
{
this._logger.LogInformation("Client connected {Session}", session.Id);
base.OnConnected(session);
}
protected override void OnDisconnected(TcpSession session)
{
this._logger.LogInformation("Client disconnected {Session}", session.Id);
base.OnDisconnected(session);
}
protected override void OnError(SocketError error) => this._logger.LogError("An error has occured {Error}", error);
}

View file

@ -1,4 +1,4 @@
services:
services:
server:
container_name: continuity-server
image: continuity:latest
@ -12,16 +12,24 @@
- DB:Port=5432
- DB:Username=continuity
- DB:Password=continuity
- Game:Data:Path=/app/data
- Game:Data:Path=/app/data/
networks:
- continuity
ports:
- "10001:10001"
- 10001:10001
volumes:
- type: bind
source: game-data
source: ../wk-data
target: /app/data
read_only: true
- type: bind
source: ../wk-logs
target: /app/logs
read_only: false
- type: bind
source: ../config
target: /app/config
read_only: true
db:
container_name: continuity-db
@ -34,11 +42,11 @@
networks:
- continuity
ports:
- "5432:5432"
- 5432:5432
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: [ "CMD-SHELL", "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

View file

@ -11,7 +11,7 @@
},
"Game":{
"Data":{
"Path": "../wk-data"
"Path": "../wk-data/"
}
}
}

2
Wiki.Dockerfile Normal file
View file

@ -0,0 +1,2 @@
FROM nginx:1.25.3-alpine3.18-slim
COPY .public /usr/share/nginx/html

View file

@ -0,0 +1,20 @@
# Character Creation Packet
## Metadata
**Operation Code**: 15
### Structure
| Identifier | Datatype | Size in bytes |
|------------|----------|---------------|
| Slot | byte | 1 |
| Unknown | byte | 1 |
| Id | ushort | 2 |
| Name | string | 20 |
| First Job | byte | 1 |
| Gender | byte | 1 |
| Hair | byte | 1 |
| Eyes | byte | 1 |
| Shirt | byte | 1 |
| Pants | byte | 1 |

View file

@ -0,0 +1,58 @@
# Character Creation Response Packet
## Metadata
**Operation Code**: 13
### Structure
Total size: 1 + 132
| Identifier | Datatype | Size in bytes |
|----------------|---------------|---------------|
| Is Duplicate | byte | 1 |
| Character data | CharacterData | 132 |
### Subtypes
#### CharacterData
Total size: 132 bytes
| Identifier | Datatype | Size in bytes |
|------------------------|------------------|---------------|
| Character Slot | int | 4 |
| Character Name | string | 20 |
| Jobs | Job Data | 4 |
| Gender | byte | 1 |
| Level | unsigned short | 2 |
| Exp? | byte | 1 |
| Stats | BaseStats | 12 |
| Health | int | 4 |
| Mana | int | 4 |
| Equipped Item Ids | unsigned short[] | 20 * 2 (40) |
| Equipped Cash Item Ids | unsigned short[] | 20 * 2 (40) |
#### Job Data
Total size: 4 bytes
| Identifier | Datatype | Size in bytes |
|------------|----------|---------------|
| First Job | byte | 1 |
| Second Job | byte | 1 |
| Third Job | byte | 1 |
| Fourth Job | byte | 1 |
#### BaseStats
Total size: 12 bytes
| Identifier | Datatype | Size in bytes |
|--------------|----------|---------------|
| Strength | short | 2 |
| Dexterity | short | 2 |
| Intelligence | short | 2 |
| Vitality | short | 2 |
| Luck | short | 2 |
| Wisdom | short | 2 |

View file

@ -7,6 +7,9 @@
<li><a href="Login-Response.md">Login Response</a></li>
<li><a href="Channel-Selection.md">Channel Selection</a></li>
<li><a href="Channel-Selection-Response.md">Channel Selection Response</a></li>
<li><a href="Character-Selection-Set-Guild-Name-Packet.md">Character Selection Set Guild Name Packet</a></li>
<li><a href="Character-Creation-Packet.md">Character Creation Packet</a></li>
<li><a href="Character-Creation-Response-Packet.md">Character Creation Response Packet</a></li>
</list>

View file

@ -7,10 +7,12 @@
<toc-element topic="Home.md"/>
<toc-element topic="Packets.md" id="packets">
<toc-element topic="Character-Creation-Response-Packet.md"/>
<toc-element topic="Character-Creation-Packet.md"/>
<toc-element topic="Channel-Selection-Response.md"/>
<toc-element topic="Channel-Selection.md"/>
<toc-element topic="Login-Info.md"/>
<toc-element topic="Login-Response.md"/>
</toc-element>
<toc-element topic="Character-Selection-Set-Guild-Name-Packet.md"/>
</toc-element>
</instance-profile>

View file

@ -2,6 +2,6 @@ namespace Wonderking.Game.Data.Item;
public struct ItemOptions
{
public ICollection<byte> OptionIDs { get; internal set; }
public uint[] OptionIDs { get; internal set; }
public bool OptionAvailable { get; internal set; }
}

View file

@ -7,7 +7,10 @@ public abstract class DataReader<T>
protected DataReader(string path)
{
Path = path;
DatFileContent = new(GetDatFileContent(path).ToArray());
_xorKey = GetXorKey();
SizeOfEntry = GetSizeOfEntry();
_datFileName = GetDatFileName();
DatFileContent = GetDatFileContent(path).ToArray();
}
private protected string Path { get; init; }
@ -15,7 +18,7 @@ public abstract class DataReader<T>
public abstract uint GetAmountOfEntries();
public abstract T GetEntry(uint entryId);
protected ushort GetSizeOfEntry()
private static ushort GetSizeOfEntry()
{
return typeof(T).GetCustomAttribute<GameDataMetadataAttribute>()?.DataEntrySize ??
throw new NotSupportedException("DataEntrySize is null");
@ -33,16 +36,20 @@ public abstract class DataReader<T>
throw new NotSupportedException("XorKey is null");
}
protected MemoryStream DatFileContent { get; }
private readonly byte _xorKey;
protected readonly ushort SizeOfEntry;
private readonly string _datFileName;
private static Span<byte> GetDatFileContent(string path)
protected byte[] DatFileContent { get; }
private Span<byte> GetDatFileContent(string path)
{
var fileData = File.ReadAllBytes(path + GetDatFileName());
var fileData = File.ReadAllBytes(path + this._datFileName);
var data = new byte[fileData.Length];
for (var i = 0; i < fileData.Length; i++)
{
data[i] = (byte)(fileData[i] ^ GetXorKey());
data[i] = (byte)(fileData[i] ^ this._xorKey);
}
return data;

View file

@ -0,0 +1,15 @@
using System.Text.Json.Serialization;
namespace Wonderking.Game.Mapping;
public class CharacterStatsMappingConfiguration
{
[JsonPropertyName("default")] public DefaultCharacterMapping DefaultCharacterMapping { get; set; }
[JsonPropertyName("1")] public JobSpecificMapping Swordsman { get; set; }
[JsonPropertyName("2")] public JobSpecificMapping Mage { get; set; }
[JsonPropertyName("3")] public JobSpecificMapping Thief { get; set; }
[JsonPropertyName("4")] public JobSpecificMapping Scout { get; set; }
}

View file

@ -0,0 +1,8 @@
using System.Text.Json.Serialization;
namespace Wonderking.Game.Mapping;
public class DefaultCharacterMapping
{
[JsonPropertyName("items")] public List<Item> Items { get; set; }
}

View file

@ -0,0 +1,47 @@
using System.Text.Json.Serialization;
namespace Wonderking.Game.Mapping;
public class DynamicStats
{
[JsonPropertyName("healthPerLevel")] public int HealthPerLevel { get; set; }
[JsonPropertyName("manaPerLevel")] public int ManaPerLevel { get; set; }
[JsonPropertyName("meleeDamagePerStrength")]
public double MeleeDamagePerStrength { get; set; }
[JsonPropertyName("rangedDamagePerDexterity")]
public double RangedDamagePerDexterity { get; set; }
[JsonPropertyName("hitRatingPerDexterity")]
public double HitRatingPerDexterity { get; set; }
[JsonPropertyName("magicPowerPerIntelligence")]
public double MagicPowerPerIntelligence { get; set; }
[JsonPropertyName("meleeDamagePerLuck")]
public double MeleeDamagePerLuck { get; set; }
[JsonPropertyName("rangedDamagePerLuck")]
public double RangedDamagePerLuck { get; set; }
[JsonPropertyName("evasionPerLuck")] public double EvasionPerLuck { get; set; }
[JsonPropertyName("criticalPerLuck")] public double CriticalPerLuck { get; set; }
[JsonPropertyName("healthPerVitality")]
public double HealthPerVitality { get; set; }
[JsonPropertyName("physicalDefensePerVitality")]
public double PhysicalDefensePerVitality { get; set; }
[JsonPropertyName("manaPerWisdom")] public double ManaPerWisdom { get; set; }
[JsonPropertyName("elementalDefensePerWisdom")]
public double ElementalDefensePerWisdom { get; set; }
[JsonPropertyName("elementalPowerPerMagicPower")]
public double ElementalPowerPerMagicPower { get; set; }
[JsonPropertyName("elementalDefensePerMagicPower")]
public double ElementalDefensePerMagicPower { get; set; }
}

View file

@ -0,0 +1,11 @@
using System.Text.Json.Serialization;
namespace Wonderking.Game.Mapping;
public class Item
{
[JsonPropertyName("id")]
public ushort Id { get; set; }
[JsonPropertyName("quantity")]
public ushort Quantity { get; set; }
}

View file

@ -0,0 +1,14 @@
using System.Text.Json.Serialization;
using Wonderking.Packets.Outgoing.Data;
namespace Wonderking.Game.Mapping;
public class JobSpecificMapping
{
[JsonPropertyName("items")]
public ICollection<Item> Items { get; set; }
[JsonPropertyName("baseStats")]
public BaseStats BaseStats { get; set; }
[JsonPropertyName("dynamicStats")]
public DynamicStats DynamicStats { get; set; }
}

View file

@ -1,107 +1,195 @@
using System.Buffers.Binary;
using System.Text;
using Wonderking.Game.Data;
using Wonderking.Game.Data.Item;
namespace Wonderking.Game.Reader;
public class ItemReader(string path) : DataReader<ItemObject>(path), IDisposable
public class ItemReader(string path) : DataReader<ItemObject>(path)
{
public override uint GetAmountOfEntries()
{
return (uint)((this.DatFileContent.Length - 9) / this.GetSizeOfEntry());
return (uint)((this.DatFileContent.Length - 9) / this.SizeOfEntry);
}
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<uint>(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
var arraySegment = new ArraySegment<byte>(DatFileContent,
9 + (int)entryId * this.SizeOfEntry, this.SizeOfEntry);
var data = new Span<byte>(arraySegment.Array, arraySegment.Offset, arraySegment.Count);
item.ItemID = BitConverter.ToUInt32(data.Slice(0, 4)); // 0 -> 4
item.Disabled = BitConverter.ToBoolean(data.Slice(4, 4)); // 4 -> 8
item.ItemType = BitConverter.ToUInt32(data.Slice(8, 4)); // 8 -> 12
item.Unknown2 = data.Slice(12, 4).ToArray(); // 12 -> 16
item.Unknown3 = data.Slice(16, 4).ToArray(); // 16 -> 20
item.ClassNo1 = BitConverter.ToUInt32(data.Slice(20, 4)); // 20 -> 24
item.ClassNo2 = BitConverter.ToUInt32(data.Slice(24, 4)); // 24 -> 28
item.ClassNo3 = BitConverter.ToUInt32(data.Slice(28, 4)); // 28 -> 32
item.ClassNo4 = BitConverter.ToUInt32(data.Slice(32, 4)); // 32 -> 36
item.SlotNo1 = BitConverter.ToUInt32(data.Slice(36, 4)); // 36 -> 40
item.SlotNo2 = BitConverter.ToUInt32(data.Slice(40, 4)); // 40 -> 44
item.Unknown4 = data.Slice(44, 4).ToArray(); // 44 -> 48
item.IsCash = BitConverter.ToUInt32(data.Slice(48, 4)); // 48 -> 52
item.Unknown5 = data.Slice(52, 4).ToArray(); // 52 -> 56
item.Price = BitConverter.ToUInt32(data.Slice(56, 4)); // 56 -> 60
item.Unknown7 = data.Slice(60, 4).ToArray(); // 60 -> 64
item.MaxNumber = BitConverter.ToUInt32(data.Slice(64, 4)); // 64 -> 68
item.Unknown17 = data.Slice(68, 12).ToArray(); // 68 -> 80
item.MaximumLevelRequirement = BitConverter.ToUInt32(data.Slice(80, 4)); // 80 -> 84
item.SexNo = BitConverter.ToUInt32(data.Slice(84, 4)); // 84 -> 88
item.WeaponSomething = BitConverter.ToUInt32(data.Slice(88, 4)); // 88 -> 92
item.Unknown8 = data.Slice(92, 4).ToArray(); // 92 -> 96
item.R2C = data.Slice(96, 16).ToArray(); // 96 -> 112
item.Unknown9 = data.Slice(112, 4).ToArray(); // 112 -> 116
item.Stats = new Stats
{
Strength = BitConverter.ToInt32(data.Slice(116, 4)), // 116 -> 120
Dexterity = BitConverter.ToInt32(data.Slice(120, 4)), // 120 -> 124
Intelligence = BitConverter.ToInt32(data.Slice(124, 4)), // 124 -> 128
Vitality = BitConverter.ToInt32(data.Slice(128, 4)), // 128 -> 132
Luck = BitConverter.ToInt32(data.Slice(132, 4)), // 132 -> 136
Wisdom = BitConverter.ToInt32(data.Slice(136, 4)), // 136 -> 140
}; // 116 -> 140
item.ElementalStats = new ElementalStats
{
MinimumFireDamage = BitConverter.ToInt32(data.Slice(140, 4)), // 140 -> 144
MinimumWaterDamage = BitConverter.ToInt32(data.Slice(144, 4)), // 144 -> 148
MinimumDarkDamage = BitConverter.ToInt32(data.Slice(148, 4)), // 148 -> 152
MinimumHolyDamage = BitConverter.ToInt32(data.Slice(152, 4)), // 152 -> 156
MaximumFireDamage = BitConverter.ToInt32(data.Slice(156, 4)), // 156 -> 160
MaximumWaterDamage = BitConverter.ToInt32(data.Slice(160, 4)), // 160 -> 164
MaximumDarkDamage = BitConverter.ToInt32(data.Slice(164, 4)), // 164 -> 168
MaximumHolyDamage = BitConverter.ToInt32(data.Slice(168, 4)), // 168 -> 172
ElementFire = BitConverter.ToUInt32(data.Slice(172, 4)), // 172 -> 176
ElementWater = BitConverter.ToUInt32(data.Slice(176, 4)), // 176 -> 180
ElementDark = BitConverter.ToUInt32(data.Slice(180, 4)), // 180 -> 184
ElementHoly = BitConverter.ToUInt32(data.Slice(184, 4)), // 184 -> 188
FireResistance = BitConverter.ToInt32(data.Slice(188, 4)), // 188 -> 192
WaterResistance = BitConverter.ToInt32(data.Slice(192, 4)), // 192 -> 196
DarkResistance = BitConverter.ToInt32(data.Slice(196, 4)), // 196 -> 200
HolyResistance = BitConverter.ToInt32(data.Slice(200, 4)), // 200 -> 204
}; // 140 -> 204
item.R7C = data.Slice(204, 4).ToArray(); // 204 -> 208
item.R8C = data.Slice(208, 8).ToArray(); // 208 -> 216
item.Speed = BinaryPrimitives.ReadSingleLittleEndian(data.Slice(216, 4)); // 216 -> 220
item.Jump = BinaryPrimitives.ReadSingleLittleEndian(data.Slice(220, 4)); // 220 -> 224
item.StatDefense = BitConverter.ToInt32(data.Slice(224, 4)); // 224 -> 228
item.MagicID = BitConverter.ToUInt32(data.Slice(228, 4)); // 228 -> 232
item.Unknown13 = data.Slice(232, 4).ToArray(); // 232 -> 236
item.Unknown14 = data.Slice(236, 4).ToArray(); // 236 -> 240
item.AdditionalHealthRecoveryVolume = BitConverter.ToInt32(data.Slice(240, 4)); // 240 -> 244
item.R9C_1 = data.Slice(244, 4).ToArray(); // 244 -> 248
item.AdditionalManaRecoveryVolume = BitConverter.ToInt32(data.Slice(248, 4)); // 248 -> 252
item.R9C_2 = data.Slice(252, 4).ToArray(); // 252 -> 256
item.R10C = data.Slice(256, 8).ToArray(); // 256 -> 264
item.AdditionalHealthPoints = BitConverter.ToInt32(data.Slice(264, 4)); // 264 -> 268
item.AdditionalManaPoints = BitConverter.ToInt32(data.Slice(268, 4)); // 268 -> 272
item.IsArrow = BitConverter.ToBoolean(data.Slice(272, 1)); // 272 -> 273
item.Unknown18 = data.Slice(273, 7).ToArray(); // 273 -> 280
item.AdditionalEvasionRate = BitConverter.ToInt32(data.Slice(280, 4)); // 280 -> 284
item.HitRate = BitConverter.ToInt32(data.Slice(284, 4)); // 284 -> 288
item.ChanceToHit = BitConverter.ToInt32(data.Slice(288, 4)); // 288 -> 292
item.MagicalDamage = BitConverter.ToInt32(data.Slice(292, 4)); // 292 -> 296
item.CriticalHitChance = BitConverter.ToInt32(data.Slice(296, 4)); // 296 -> 300
item.R12C = data.Slice(300, 4).ToArray(); // 300 -> 304
item.Unknown16 = data.Slice(304, 4).ToArray(); // 304 -> 308
item.MinimalAttackDamage = BitConverter.ToInt32(data.Slice(308, 4)); // 308 -> 312
item.MaximalAttackDamage = BitConverter.ToInt32(data.Slice(312, 4)); // 312 -> 316
item.PhysicalDamage = BitConverter.ToInt32(data.Slice(316, 4)); // 316 -> 320
item.CraftMaterial = new CraftMaterial[]
{
new()
{
ID = BitConverter.ToUInt32(data.Slice(320, 4)), // 320 -> 324
Amount = BitConverter.ToUInt32(data.Slice(336, 4)) // 336 -> 340
},
new()
{
ID = BitConverter.ToUInt32(data.Slice(324, 4)), // 324 -> 328
Amount = BitConverter.ToUInt32(data.Slice(340, 4)) // 340 -> 344
},
new()
{
ID = BitConverter.ToUInt32(data.Slice(328, 4)), // 328 -> 332
Amount = BitConverter.ToUInt32(data.Slice(344, 4)) // 344 -> 348
},
new()
{
ID = BitConverter.ToUInt32(data.Slice(332, 4)), // 332 -> 336
Amount = BitConverter.ToUInt32(data.Slice(348, 4)) // 348 -> 352
},
}; // 320 -> 352
item.CraftResultAmount = BitConverter.ToUInt32(data.Slice(352, 4)); // 352 -> 356
item.R14C = data.Slice(356, 4).ToArray(); // 356 -> 360
item.CraftResultItem = BitConverter.ToUInt32(data.Slice(360, 4)); // 360 -> 364
item.R15C = data.Slice(364, 4).ToArray(); // 364 -> 368
item.R16C = data.Slice(368, 20).ToArray(); // 368 -> 388
item.InventoryX = BitConverter.ToInt32(data.Slice(388, 4)); // 388 -> 392
item.InventoryY = BitConverter.ToInt32(data.Slice(392, 4)); // 392 -> 396
item.InventoryWidth = BitConverter.ToInt32(data.Slice(396, 4)); // 396 -> 400
item.InventoryHeight = BitConverter.ToInt32(data.Slice(400, 4)); // 400 -> 404
item.SheetID = BitConverter.ToInt32(data.Slice(404, 4)); // 404 -> 408
item.Name = Encoding.ASCII.GetString(data.Slice(408, 20)); // 408 -> 428
item.Description = Encoding.ASCII.GetString(data.Slice(428, 85)); // 428 -> 513
item.Unknown1 = data.Slice(513, 175).ToArray(); // 513 -> 688
item.IsEnchantable = BitConverter.ToBoolean(data.Slice(688, 4)); // 688 -> 672
item.Unknown1_2 = data.Slice(692, 104).ToArray(); // 692 -> 796
item.SetItems = new[]
{
BitConverter.ToUInt32(data.Slice(796, 4)), // 796 -> 800
BitConverter.ToUInt32(data.Slice(800, 4)), // 800 -> 804
BitConverter.ToUInt32(data.Slice(804, 4)), // 804 -> 808
BitConverter.ToUInt32(data.Slice(808, 4)), // 808 -> 812
BitConverter.ToUInt32(data.Slice(812, 4)), // 812 -> 816
}; // 796 -> 816
item.SetID = BitConverter.ToUInt32(data.Slice(816, 4)); // 816 -> 820
item.Options = new ItemOptions
{
OptionIDs = new[]
{
BitConverter.ToUInt32(data.Slice(824, 4)), // 824 -> 828
BitConverter.ToUInt32(data.Slice(828, 4)), // 828 -> 832
BitConverter.ToUInt32(data.Slice(832, 4)), // 832 -> 836
BitConverter.ToUInt32(data.Slice(836, 4)), // 836 -> 840
},
OptionAvailable = BitConverter.ToBoolean(data.Slice(820, 4)), // 820 -> 824
}; // 820 -> 840
item.Unknown19 = data.Slice(840, 23).ToArray(); // 840 -> 863
item.PetID = data[863]; // 863 -> 864
item.Unknown20 = data.Slice(864, 20).ToArray(); // 864 -> 884
item.HitBoxScaling = data[884]; // 884 -> 885
item.Unknown20_2 = data.Slice(885, 13).ToArray(); // 885 -> 898
item.ContainedItems = new[]
{
new ContainedItem
{
ID = BitConverter.ToInt16(data.Slice(898, 2)), // 898 -> 900
ObtainChance = BitConverter.ToSingle(data.Slice(908, 4)) // 908 -> 912
},
new ContainedItem
{
ID = BitConverter.ToInt16(data.Slice(900, 2)), // 900 -> 902
ObtainChance = BitConverter.ToSingle(data.Slice(912, 4)) // 912 -> 916
},
new ContainedItem
{
ID = BitConverter.ToInt16(data.Slice(902, 2)), // 902 -> 904
ObtainChance = BitConverter.ToSingle(data.Slice(916, 4)) // 916 -> 920
},
new ContainedItem
{
ID = BitConverter.ToInt16(data.Slice(904, 2)), // 904 -> 906
ObtainChance = BitConverter.ToSingle(data.Slice(920, 4)) // 920 -> 924
},
new ContainedItem
{
ID = BitConverter.ToInt16(data.Slice(906, 2)), // 906 -> 908
ObtainChance = BitConverter.ToSingle(data.Slice(924, 4)) // 924 -> 928
},
};
item.MinimumLevelRequirement = data[928]; // 928 -> 929
item.Unknown21_2 = data.Slice(929, 3).ToArray(); // 929 -> 932
return item;
}
public void Dispose()
{
this.DatFileContent.Dispose();
}
}

View file

@ -82,14 +82,14 @@ public static class ItemReaderExtensions
options.OptionAvailable = reader.ReadInt32() == 1; //819
var optionIDs = new List<byte>(4);
var optionIDs = new List<uint>(4);
//823
for (var i = 0; i < 3; i++)
{
optionIDs.Add((byte)reader.ReadUInt32());
optionIDs.Add(reader.ReadUInt32());
}
options.OptionIDs = optionIDs;
options.OptionIDs = optionIDs.ToArray();
return options;
}

View file

@ -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();
}
}

View file

@ -0,0 +1,24 @@
using System.Text;
namespace Wonderking.Packets.Incoming;
[PacketId(OperationCode.CharacterDeletion)]
public class CharacterDeletePacket : IPacket
{
public byte Slot { get; set; }
public string Name { get; set; }
public uint Unknown { get; set; }
public void Deserialize(byte[] data)
{
Span<byte> span = data;
Slot = span[0];
Name = Encoding.ASCII.GetString(span.Slice(1, 20)).TrimEnd('\0').TrimEnd('\n').TrimEnd('\0');
Unknown = BitConverter.ToUInt32(span.Slice(21, 4));
}
public byte[] Serialize()
{
throw new NotSupportedException();
}
}

View file

@ -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)
{
Name = Encoding.ASCII.GetString(data, 0, 20).TrimEnd('\0').TrimEnd('\n').TrimEnd('\0');
}
public byte[] Serialize()
{
throw new NotSupportedException();
}
}

View file

@ -6,5 +6,12 @@ public enum OperationCode : ushort
LoginResponse = 12,
ChannelSelection = 13,
ChannelSelectionResponse = 13,
CharacterNameCheck = 14,
CharacterNameCheckResponse = 14,
CharacterCreation = 15,
CharacterCreationResponse = 15,
CharacterDeletion = 16,
CharacterDeletionResponse = 16,
CharacterSelection = 17,
CharacterSelectionSetGuildName = 19,
}

View file

@ -0,0 +1,60 @@
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 required int Slot { get; set; }
public required bool isDuplicate { get; set; }
public void Deserialize(byte[] data)
{
throw new NotSupportedException();
}
public byte[] Serialize()
{
Span<byte> data = stackalloc byte[1 + 132];
data[0] = isDuplicate ? (byte)1 : (byte)0;
BinaryPrimitives.WriteInt32LittleEndian(data.Slice(1, 4), Slot);
Encoding.ASCII.GetBytes(Character.Name, data.Slice(5, 20));
// Job Data
data[25] = Character.Job.FirstJob;
data[26] = Character.Job.SecondJob;
data[27] = Character.Job.ThirdJob;
data[28] = Character.Job.FourthJob;
data[29] = (byte)Character.Gender;
BinaryPrimitives.WriteUInt16LittleEndian(data.Slice(30, 2), Character.Level);
data[32] = (byte)Character.Experience;
// Stats
BinaryPrimitives.WriteInt16LittleEndian(data.Slice(33, 2), Character.Stats.Strength);
BinaryPrimitives.WriteInt16LittleEndian(data.Slice(35, 2), Character.Stats.Dexterity);
BinaryPrimitives.WriteInt16LittleEndian(data.Slice(37, 2), Character.Stats.Intelligence);
BinaryPrimitives.WriteInt16LittleEndian(data.Slice(39, 2), Character.Stats.Vitality);
BinaryPrimitives.WriteInt16LittleEndian(data.Slice(41, 2), Character.Stats.Luck);
BinaryPrimitives.WriteInt16LittleEndian(data.Slice(43, 2), Character.Stats.Wisdom);
BinaryPrimitives.WriteInt32LittleEndian(data.Slice(45, 4), Character.Health);
BinaryPrimitives.WriteInt32LittleEndian(data.Slice(49, 4), Character.Mana);
for (var i = 0; i < 20; i++)
{
// Equipped Items
BinaryPrimitives.WriteUInt16LittleEndian(data.Slice(53 + i * 2, 2),
Character.EquippedItems.Length > i ? Character.EquippedItems[i] : (ushort)0);
// Equipped Cash Items
BinaryPrimitives.WriteUInt16LittleEndian(data.Slice(93 + i * 2, 2),
Character.EquippedCashItems.Length > i ? Character.EquippedCashItems[i] : (ushort)0);
}
return data.ToArray();
}
}

View file

@ -0,0 +1,19 @@
namespace Wonderking.Packets.Outgoing;
[PacketId(OperationCode.CharacterDeletionResponse)]
public class CharacterDeleteResponsePacket : IPacket
{
public required byte IsDeleted { get; set; }
public void Deserialize(byte[] data)
{
throw new NotImplementedException();
}
public byte[] Serialize()
{
Span<byte> data = stackalloc byte[1];
data[0] = IsDeleted;
return data.ToArray();
}
}

View file

@ -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<byte> data = stackalloc byte[1];
data[0] = this.IsTaken ? (byte)1 : (byte)0;
return data.ToArray();
}
}

View file

@ -1,14 +1,17 @@
using System.Text.Json.Serialization;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore;
namespace Wonderking.Packets.Outgoing.Data;
[UsedImplicitly]
[Owned]
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; }
[JsonPropertyName("strength")] public required short Strength { get; set; }
[JsonPropertyName("dexterity")] public required short Dexterity { get; set; }
[JsonPropertyName("intelligence")] public required short Intelligence { get; set; }
[JsonPropertyName("vitality")] public required short Vitality { get; set; }
[JsonPropertyName("luck")] public required short Luck { get; set; }
[JsonPropertyName("wisdom")] public required short Wisdom { get; set; }
}

View file

@ -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; }

View file

@ -1,3 +1,4 @@
using System.Buffers.Binary;
using Wonderking.Packets.Outgoing.Data;
namespace Wonderking.Packets.Outgoing;
@ -34,9 +35,7 @@ public class LoginResponsePacket : IPacket
dataSpan[0] = (byte)this.ResponseReason;
dataSpan[1] = this.UnknownFlag;
dataSpan[2] = BitConverter.GetBytes(this.IsGameMaster)[0];
var bytesOfChannelAmount = BitConverter.GetBytes((ushort)this.ChannelData.Length);
dataSpan[3] = bytesOfChannelAmount[0];
dataSpan[4] = bytesOfChannelAmount[1];
BinaryPrimitives.WriteUInt16LittleEndian(dataSpan.Slice(3, 2), (ushort)this.ChannelData.Length);
for (var i = 0; i < this.ChannelData.Length; i++)
{

View file

@ -22,6 +22,7 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.8.14">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View file

@ -0,0 +1,150 @@
{
"default": {
"items": [
{
"id": 841,
"quantity": 1
}
]
},
"1": {
"items": [
{
"id": 70,
"quantity": 1
}
],
"baseStats": {
"strength": 13,
"dexterity": 9,
"intelligence": 3,
"luck": 5,
"vitality": 17,
"wisdom": 5
},
"dynamicStats": {
"healthPerLevel": 20,
"manaPerLevel": 8,
"meleeDamagePerStrength": 0.4,
"rangedDamagePerDexterity": 0,
"hitRatingPerDexterity": 4.2,
"magicPowerPerIntelligence": 0.55,
"meleeDamagePerLuck": 0,
"rangedDamagePerLuck": 0,
"evasionPerLuck": 1.35,
"criticalPerLuck": 0.25,
"healthPerVitality": 5.75,
"physicalDefensePerVitality": 0.11,
"manaPerWisdom": 3.4,
"elementalDefensePerWisdom": 0.045,
"elementalPowerPerMagicPower": 0.4,
"elementalDefensePerMagicPower": 0.1
}
},
"2": {
"items": [
{
"id": 150,
"quantity": 1
}
],
"baseStats": {
"strength": 6,
"dexterity": 6,
"intelligence": 14,
"luck": 3,
"vitality": 16,
"wisdom": 11
},
"dynamicStats": {
"healthPerLevel": 12,
"manaPerLevel": 16,
"meleeDamagePerStrength": 0.3,
"rangedDamagePerDexterity": 0,
"hitRatingPerDexterity": 6.2,
"magicPowerPerIntelligence": 0.85,
"meleeDamagePerLuck": 0,
"rangedDamagePerLuck": 0,
"evasionPerLuck": 1.35,
"criticalPerLuck": 0.25,
"healthPerVitality": 3.45,
"physicalDefensePerVitality": 0.04,
"manaPerWisdom": 3.6,
"elementalDefensePerWisdom": 0.29,
"elementalPowerPerMagicPower": 0.4,
"elementalDefensePerMagicPower": 0.1
}
},
"3": {
"items": [
{
"id": 218,
"quantity": 1
}
],
"baseStats": {
"strength": 7,
"dexterity": 11,
"intelligence": 59,
"luck": 5,
"vitality": 15,
"wisdom": 6
},
"dynamicStats": {
"healthPerLevel": 13,
"manaPerLevel": 12,
"meleeDamagePerStrength": 0,
"rangedDamagePerDexterity": 0,
"hitRatingPerDexterity": 4.2,
"magicPowerPerIntelligence": 0.55,
"meleeDamagePerLuck": 0.3,
"rangedDamagePerLuck": 0.15,
"evasionPerLuck": 1.5,
"criticalPerLuck": 0.25,
"healthPerVitality": 4.55,
"physicalDefensePerVitality": 0.04,
"manaPerWisdom": 3.4,
"elementalDefensePerWisdom": 0.065,
"elementalPowerPerMagicPower": 0.4,
"elementalDefensePerMagicPower": 0.1
}
},
"4": {
"items": [
{
"id": 299,
"quantity": 1
},
{
"id": 305,
"quantity": 1000
}
],
"baseStats": {
"strength": 6,
"dexterity": 13,
"intelligence": 8,
"luck": 5,
"vitality": 8,
"wisdom": 7
},
"dynamicStats": {
"healthPerLevel": 12,
"manaPerLevel": 15,
"meleeDamagePerStrength": 0,
"rangedDamagePerDexterity": 0.25,
"hitRatingPerDexterity": 3.0,
"magicPowerPerIntelligence": 0.55,
"meleeDamagePerLuck": 0,
"rangedDamagePerLuck": 0,
"evasionPerLuck": 1.35,
"criticalPerLuck": 0.25,
"healthPerVitality": 4.55,
"physicalDefensePerVitality": 0.1,
"manaPerWisdom": 3.5,
"elementalDefensePerWisdom": 0.055,
"elementalPowerPerMagicPower": 0.4,
"elementalDefensePerMagicPower": 0.1
}
}
}

View file

@ -0,0 +1,14 @@
$SourceFilePath = $args[0]
$DestinationFilePath = $args[1]
$XorKey = [byte]$args[2]
# Load the binary data from the file
$data = [System.IO.File]::ReadAllBytes($SourceFilePath)
# Apply the XOR operation
for ($i = 0; $i -lt $data.Length; $i++) {
$data[$i] = $data[$i] -bxor $XorKey
}
# Write the output to the destination file
[System.IO.File]::WriteAllBytes($DestinationFilePath, $data)

View file

@ -0,0 +1,36 @@
$SourceFilePath = $args[0]
$ChunkSize = $args[1]
$Offset = $args[2]
# Function to create chunk file
function Create-ChunkFile {
param (
[byte[]]$ChunkData,
[int]$ChunkNumber
)
$chunkFileName = Join-Path $subDir ("{0}_{1:D5}.bin" -f [System.IO.Path]::GetFileNameWithoutExtension($SourceFilePath), $ChunkNumber)
[System.IO.File]::WriteAllBytes($chunkFileName, $ChunkData)
}
# Create subdirectory named after the source file (without extension) for chunks
$subDir = Join-Path ([System.IO.Path]::GetDirectoryName($SourceFilePath)) ([System.IO.Path]::GetFileNameWithoutExtension($SourceFilePath))
if (!(Test-Path -Path $subDir)) {
New-Item -ItemType Directory -Path $subDir
}
# Read the binary data from the file
$data = [System.IO.File]::ReadAllBytes($SourceFilePath)
$dataLength = $data.Length
$chunkNumber = 0
# Adjust the start position based on the offset
$startPosition = [Math]::Min($Offset, $dataLength)
# Process chunks
for ($i = $startPosition; $i -lt $dataLength; $i += $ChunkSize) {
$chunkEnd = [Math]::Min($i + $ChunkSize, $dataLength)
$chunkData = $data[$i..($chunkEnd - 1)]
Create-ChunkFile -ChunkData $chunkData -ChunkNumber $chunkNumber
$chunkNumber++
}