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 run-name: ${{ gitea.actor }} is building the Server application
on: [ push ] on:
push:
env: paths-ignore:
# Name of module and id separated by a slash - Wiki/**
INSTANCE: Wiki/wiki - Benchmarks/**
# Replace HI with the ID of the instance in capital letters - .run/**
ARTIFACT: webHelpWIKI2-all.zip
# Writerside docker image version
DOCKER_VERSION: 232.10165.1
ALGOLIA_ARTIFACT: algolia-indexes-wiki.zip
jobs: jobs:
preprocess: preprocess:
@ -18,217 +13,167 @@ jobs:
outputs: outputs:
sanitized_branch_name: ${{ steps.sanitize.outputs.sanitized_branch_name }} sanitized_branch_name: ${{ steps.sanitize.outputs.sanitized_branch_name }}
steps: steps:
- name: Sanitize branch name - name: Sanitize branch name
id: sanitize id: sanitize
run: echo "::set-output name=sanitized_branch_name::$(echo ${{ github.ref_name }} | sed 's/\//-/g')" 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: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Setup dotnet - name: Setup dotnet
uses: https://github.com/actions/setup-dotnet@v3 uses: https://github.com/actions/setup-dotnet@v3
with: with:
dotnet-version: 8.0 dotnet-version: 8.0
- name: Install dependencies - name: Install dependencies
run: dotnet restore run: dotnet restore
- name: Build - name: Build
run: | run: |
dotnet build Server -c Release dotnet build Server -c Release
# dotnet test Server.Tests -c Release # dotnet test Server.Tests -c Release
sonarqube: sonarqube:
needs: build needs: build
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.ref_name == 'master' if: github.ref_name == 'master'
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Setup dotnet - name: Setup dotnet
uses: https://github.com/actions/setup-dotnet@v3 uses: https://github.com/actions/setup-dotnet@v3
with: with:
dotnet-version: 8.0 dotnet-version: 8.0
- name: Install dependencies - name: Install dependencies
run: | run: |
dotnet restore dotnet restore
echo "::add-path::$HOME/.dotnet/tools" echo "::add-path::$HOME/.dotnet/tools"
- name: Setup Sonarqube Dependencies - name: Setup Sonarqube Dependencies
run: | run: |
apt-get update apt-get update
apt-get install --yes openjdk-11-jre apt-get install --yes openjdk-11-jre
dotnet tool install --global dotnet-sonarscanner dotnet tool install --global dotnet-sonarscanner
dotnet tool install --global dotnet-coverage dotnet tool install --global dotnet-coverage
- name: Sonarqube Begin - name: Sonarqube Begin
run: | run: |
dotnet sonarscanner begin /key:"${{ secrets.SONAR_PROJECT_KEY }}" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="${{ secrets.SONAR_HOST_URL }}" dotnet sonarscanner begin /key:"${{ secrets.SONAR_PROJECT_KEY }}" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="${{ secrets.SONAR_HOST_URL }}"
- name: Sonarqube Scan - name: Sonarqube Scan
run: | run: |
dotnet build Server -c Release dotnet build Server -c Release
# dotnet test --collect "Code Coverage" --logger trx --results-directory "TestsResults" # dotnet test --collect "Code Coverage" --logger trx --results-directory "TestsResults"
# dotnet-coverage collect 'dotnet test' -f xml -o 'coverage.xml' # dotnet-coverage collect 'dotnet test' -f xml -o 'coverage.xml'
- name: Sonarqube End - name: Sonarqube End
run: | run: |
dotnet sonarscanner end /d:sonar.login="${{ secrets.SONAR_TOKEN }}" dotnet sonarscanner end /d:sonar.login="${{ secrets.SONAR_TOKEN }}"
sbom-scan: sbom-scan:
needs: build needs: build
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Setup dotnet - name: Setup dotnet
uses: https://github.com/actions/setup-dotnet@v3 uses: https://github.com/actions/setup-dotnet@v3
with: with:
dotnet-version: | dotnet-version: |
7.0 7.0
8.0 8.0
- name: Install dependencies - name: Install dependencies
run: | run: |
dotnet restore dotnet restore
echo "::add-path::$HOME/.dotnet/tools" echo "::add-path::$HOME/.dotnet/tools"
- name: Setup Dependency Track Dependencies - name: Setup Dependency Track Dependencies
run: | run: |
dotnet tool install --global CycloneDX dotnet tool install --global CycloneDX
- name: Generate SBOM - name: Generate SBOM
run: | run: |
dotnet CycloneDX Server/Server.csproj -o . -dgl dotnet CycloneDX Server/Server.csproj -o . -dgl
- name: Upload SBOM - name: Upload SBOM
uses: https://github.com/DependencyTrack/gh-upload-sbom@v2.0.1 uses: https://github.com/DependencyTrack/gh-upload-sbom@v2.0.1
with: with:
apiKey: ${{ secrets.DEPENDENCY_TRACK_API_KEY }} apiKey: ${{ secrets.DEPENDENCY_TRACK_API_KEY }}
serverHostname: ${{ secrets.DEPENDENCY_TRACK_URL }} serverHostname: ${{ secrets.DEPENDENCY_TRACK_URL }}
projectName: ${{ secrets.DEPENDENCY_TRACK_PROJECT_NAME }} projectName: ${{ secrets.DEPENDENCY_TRACK_PROJECT_NAME }}
autoCreate: true autoCreate: true
# set projectversion to be the branch name # set projectversion to be the branch name
projectVersion: "${{ github.ref_name }}" projectVersion: ${{ github.ref_name }}
bomFilename: "${{ github.workspace }}/bom.xml" bomFilename: ${{ github.workspace }}/bom.xml
container-build: container-build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: catthehacker/ubuntu:act-latest container: catthehacker/ubuntu:act-latest
needs: [ build, preprocess ] needs: [build, preprocess]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Setup dotnet - name: Setup dotnet
uses: https://github.com/actions/setup-dotnet@v3 uses: https://github.com/actions/setup-dotnet@v3
with: with:
dotnet-version: 8.0 dotnet-version: 8.0
# Add support for more platforms with QEMU (optional) # Add support for more platforms with QEMU (optional)
# https://github.com/docker/setup-qemu-action # https://github.com/docker/setup-qemu-action
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: ${{ github.server_url }} registry: ${{ github.server_url }}
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.REGISTRY_TOKEN }} password: ${{ secrets.REGISTRY_TOKEN }}
- name: Build and push - name: Build and push
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
with: with:
context: . context: .
file: Server/Dockerfile file: Server/Dockerfile
push: true push: true
tags: forge.rainote.dev/${{ github.repository }}:${{ needs.preprocess.outputs.sanitized_branch_name }} tags: forge.rainote.dev/${{ github.repository }}:${{ needs.preprocess.outputs.sanitized_branch_name }}
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
- name: Build and push to latest - name: Build and push to latest
if: github.ref_name == 'master' if: github.ref_name == 'master'
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
with: with:
context: . context: .
file: Server/Dockerfile file: Server/Dockerfile
push: true push: true
tags: forge.rainote.dev/${{ github.repository }}:latest tags: forge.rainote.dev/${{ github.repository }}:latest
platforms: linux/amd64, linux/arm64 platforms: linux/amd64, linux/arm64
container-sbom-scan: container-sbom-scan:
needs: [ container-build, preprocess ] needs: [container-build, preprocess]
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: catthehacker/ubuntu:act-latest container: catthehacker/ubuntu:act-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Setup dotnet - name: Setup dotnet
uses: https://github.com/actions/setup-dotnet@v3 uses: https://github.com/actions/setup-dotnet@v3
with: with:
dotnet-version: 8.0 dotnet-version: 8.0
- name: Install dependencies - name: Install dependencies
run: | run: |
dotnet restore dotnet restore
echo "::add-path::$HOME/.dotnet/tools" echo "::add-path::$HOME/.dotnet/tools"
- name: Setup Dependency Track Dependencies - name: Setup Dependency Track Dependencies
run: | run: |
mkdir ~/.docker mkdir ~/.docker
curl -sSfL https://raw.githubusercontent.com/docker/sbom-cli-plugin/main/install.sh | sh -s -- curl -sSfL https://raw.githubusercontent.com/docker/sbom-cli-plugin/main/install.sh | sh -s --
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: ${{ github.server_url }} registry: ${{ github.server_url }}
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.REGISTRY_TOKEN }} password: ${{ secrets.REGISTRY_TOKEN }}
- name: Generate SBOM - name: Generate SBOM
run: | run: |
echo forge.rainote.dev/${{ github.repository }} echo forge.rainote.dev/${{ github.repository }}
echo forge.rainote.dev/${{ github.repository }}:${{ needs.preprocess.outputs.sanitized_branch_name }} echo forge.rainote.dev/${{ github.repository }}:${{ needs.preprocess.outputs.sanitized_branch_name }}
docker pull forge.rainote.dev/${{ github.repository }}:${{ needs.preprocess.outputs.sanitized_branch_name }} docker pull forge.rainote.dev/${{ github.repository }}:${{ needs.preprocess.outputs.sanitized_branch_name }}
docker sbom -D forge.rainote.dev/${{ github.repository }}:${{ needs.preprocess.outputs.sanitized_branch_name }} --format cyclonedx-json --output container-bom.json docker sbom -D forge.rainote.dev/${{ github.repository }}:${{ needs.preprocess.outputs.sanitized_branch_name }} --format cyclonedx-json --output container-bom.json
- name: Upload SBOM - name: Upload SBOM
uses: https://github.com/DependencyTrack/gh-upload-sbom@v2.0.1 uses: https://github.com/DependencyTrack/gh-upload-sbom@v2.0.1
with: with:
apiKey: ${{ secrets.DEPENDENCY_TRACK_API_KEY }} apiKey: ${{ secrets.DEPENDENCY_TRACK_API_KEY }}
serverHostname: ${{ secrets.DEPENDENCY_TRACK_URL }} serverHostname: ${{ secrets.DEPENDENCY_TRACK_URL }}
projectName: "${{ secrets.DEPENDENCY_TRACK_PROJECT_NAME }}-container" projectName: ${{ secrets.DEPENDENCY_TRACK_PROJECT_NAME }}-container
autoCreate: true autoCreate: true
# set projectversion to be the branch name # set projectversion to be the branch name
projectVersion: "${{ github.ref_name }}" projectVersion: ${{ github.ref_name }}
bomFilename: "${{ github.workspace }}/container-bom.json" bomFilename: ${{ github.workspace }}/container-bom.json

View file

@ -1,9 +1,24 @@
repos: repos:
- repo: local - repo: local
hooks: hooks:
#Use dotnet format already installed on your machine #Use dotnet format already installed on your machine
- id: dotnet-format - id: dotnet-format
name: dotnet-format name: dotnet-format
language: system language: system
entry: dotnet format --include 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; using Wonderking.Packets;
namespace Server; namespace Server;
@ -31,7 +32,7 @@ public class AuthSession : TcpSession
public void Send(IPacket packet) public void Send(IPacket packet)
{ {
var type = packet.GetType(); 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>(); var packetIdAttribute = type.GetCustomAttribute<PacketIdAttribute>();
if (packetIdAttribute == null) if (packetIdAttribute == null)
{ {
@ -59,8 +60,8 @@ public class AuthSession : TcpSession
buffer[2 + i] = bytesOfOpcode[i]; buffer[2 + i] = bytesOfOpcode[i];
} }
this._logger.LogTrace("Packet data being parsed is: {Data}", BitConverter.ToString(packetData.ToArray())); this._logger.LogInformation("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 being parsed is: {Data}", BitConverter.ToString(buffer.ToArray()));
this.Send(buffer); this.Send(buffer);
} }
@ -105,4 +106,9 @@ public class AuthSession : TcpSession
return buffer; 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; namespace Server.DB.Documents;
[Index(nameof(Username), IsUnique = true), Index(nameof(Id), IsUnique = true)]
public class Account public class Account
{ {
public Account(string username, byte[] password, string email, byte permissionLevel, byte[] salt) public Account(string username, byte[] password, string email, byte permissionLevel, byte[] salt)
@ -11,12 +16,18 @@ public class Account
this.Salt = salt; this.Salt = salt;
} }
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid Id { get; set; } public Guid Id { get; set; }
[Column(TypeName = "varchar(20)")]
[MaxLength(20)]
public string Username { get; set; } 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 PermissionLevel { get; set; }
public byte[] Salt { get; set; } [Column(TypeName = "bytea")] public byte[] Salt { get; set; }
public ICollection<Character> Characters { get; } = new List<Character>(); 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.Game.Data.Character;
using Wonderking.Packets.Outgoing.Data; using Wonderking.Packets.Outgoing.Data;
namespace Server.DB.Documents; namespace Server.DB.Documents;
[Index(nameof(Name), IsUnique = true), Index(nameof(Id), IsUnique = true)]
public class Character public class Character
{ {
public byte ServerId { get; set; } public virtual Account Account { get; set; }
public Guid AccountId { get; set; }
public Account Account { get; set; } [Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid Id { get; set; } public Guid Id { get; set; }
public ushort MapId { get; set; } public ushort MapId { get; set; }
[Column(TypeName = "varchar(20)")]
[MaxLength(20)]
public string Name { get; set; } public string Name { get; set; }
public short LastXCoordinate { get; set; } public short LastXCoordinate { get; set; }
public short LastYCoordinate { get; set; } public short LastYCoordinate { get; set; }
public PvPLevel PvPLevel { get; set; } public PvPLevel PvPLevel { get; set; }
public Gender Gender { get; set; } public Gender Gender { get; set; }
public long Experience { get; set; } public long Experience { get; set; }
public byte Level { 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 BaseStats BaseStats { get; set; }
public JobData JobData { get; set; } public JobData JobData { get; set; }
public int Health { get; set; } public int Health { get; set; }
public int Mana { get; set; } public int Mana { get; set; }
public Guid GuildId { get; set; } public virtual Guild Guild { get; set; }
public Guild Guild { get; set; }
} }

View file

@ -1,9 +1,15 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Server.DB.Documents; namespace Server.DB.Documents;
public class Guild public class Guild
{ {
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid Id { get; set; } public Guid Id { get; set; }
public string Name { get; set; } public string Name { get; set; }
public string Notice { 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; namespace Server.DB.Documents;
public class GuildMember public class GuildMember
{ {
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid Id { get; set; } public Guid Id { get; set; }
public Guid CharacterId { get; set; }
public Character Character { get; set; } public virtual Character Character { get; set; }
public Guid GuildId { get; set; } public virtual Guild Guild { get; set; }
public Guild Guild { get; set; }
public GuildRank Rank { get; set; } public GuildRank Rank { get; set; }
} }

View file

@ -3,12 +3,12 @@ namespace Server.DB.Documents;
public class InventoryItem public class InventoryItem
{ {
public Guid CharacterId { get; set; } public Guid CharacterId { get; set; }
public Character Character { get; set; } public virtual Character Character { get; set; }
public Guid Id { get; set; } public Guid Id { get; set; }
public ushort ItemId { get; set; } public ushort ItemId { get; set; }
public ushort Count { get; set; } public ushort Count { get; set; }
public byte Slot { get; set; } public byte Slot { get; set; }
public ItemType ItemType { get; set; } public InventoryTab InventoryTab { get; set; }
public byte Level { get; set; } public byte Level { get; set; }
public byte Rarity { get; set; } public byte Rarity { get; set; }
public byte AddOption { get; set; } public byte AddOption { get; set; }

View file

@ -1,6 +1,6 @@
namespace Server.DB.Documents; namespace Server.DB.Documents;
public enum ItemType : byte public enum InventoryTab : byte
{ {
WornEquipment = 0, WornEquipment = 0,
WornCashEquipment = 1, 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 #pragma warning disable 612, 618
modelBuilder modelBuilder
.HasAnnotation("ProductVersion", "8.0.0") .HasAnnotation("ProductVersion", "8.0.0")
.HasAnnotation("Proxies:ChangeTracking", false)
.HasAnnotation("Proxies:CheckEquality", false)
.HasAnnotation("Proxies:LazyLoading", true)
.HasAnnotation("Relational:MaxIdentifierLength", 63); .HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
@ -41,10 +44,14 @@ namespace Server.DB.Migrations
.HasColumnType("bytea"); .HasColumnType("bytea");
b.Property<string>("Username") b.Property<string>("Username")
.HasMaxLength(20)
.HasColumnType("varchar(20)"); .HasColumnType("varchar(20)");
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("Id")
.IsUnique();
b.HasIndex("Username") b.HasIndex("Username")
.IsUnique(); .IsUnique();
@ -57,7 +64,7 @@ namespace Server.DB.Migrations
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("uuid"); .HasColumnType("uuid");
b.Property<Guid>("AccountId") b.Property<Guid?>("AccountId")
.HasColumnType("uuid"); .HasColumnType("uuid");
b.Property<long>("Experience") b.Property<long>("Experience")
@ -66,7 +73,7 @@ namespace Server.DB.Migrations
b.Property<byte>("Gender") b.Property<byte>("Gender")
.HasColumnType("smallint"); .HasColumnType("smallint");
b.Property<Guid>("GuildId") b.Property<Guid?>("GuildId")
.HasColumnType("uuid"); .HasColumnType("uuid");
b.Property<int>("Health") b.Property<int>("Health")
@ -88,20 +95,21 @@ namespace Server.DB.Migrations
.HasColumnType("integer"); .HasColumnType("integer");
b.Property<string>("Name") b.Property<string>("Name")
.HasMaxLength(20)
.HasColumnType("varchar(20)"); .HasColumnType("varchar(20)");
b.Property<byte>("PvPLevel") b.Property<byte>("PvPLevel")
.HasColumnType("smallint"); .HasColumnType("smallint");
b.Property<byte>("ServerId")
.HasColumnType("smallint");
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("AccountId"); b.HasIndex("AccountId");
b.HasIndex("GuildId"); b.HasIndex("GuildId");
b.HasIndex("Id")
.IsUnique();
b.HasIndex("Name") b.HasIndex("Name")
.IsUnique(); .IsUnique();
@ -131,10 +139,10 @@ namespace Server.DB.Migrations
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("uuid"); .HasColumnType("uuid");
b.Property<Guid>("CharacterId") b.Property<Guid?>("CharacterId")
.HasColumnType("uuid"); .HasColumnType("uuid");
b.Property<Guid>("GuildId") b.Property<Guid?>("GuildId")
.HasColumnType("uuid"); .HasColumnType("uuid");
b.Property<byte>("Rank") b.Property<byte>("Rank")
@ -170,12 +178,12 @@ namespace Server.DB.Migrations
b.Property<int>("Count") b.Property<int>("Count")
.HasColumnType("integer"); .HasColumnType("integer");
b.Property<byte>("InventoryTab")
.HasColumnType("smallint");
b.Property<int>("ItemId") b.Property<int>("ItemId")
.HasColumnType("integer"); .HasColumnType("integer");
b.Property<byte>("ItemType")
.HasColumnType("smallint");
b.Property<byte>("Level") b.Property<byte>("Level")
.HasColumnType("smallint"); .HasColumnType("smallint");
@ -205,15 +213,11 @@ namespace Server.DB.Migrations
{ {
b.HasOne("Server.DB.Documents.Account", "Account") b.HasOne("Server.DB.Documents.Account", "Account")
.WithMany("Characters") .WithMany("Characters")
.HasForeignKey("AccountId") .HasForeignKey("AccountId");
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Server.DB.Documents.Guild", "Guild") b.HasOne("Server.DB.Documents.Guild", "Guild")
.WithMany() .WithMany()
.HasForeignKey("GuildId") .HasForeignKey("GuildId");
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.OwnsOne("Wonderking.Packets.Outgoing.Data.BaseStats", "BaseStats", b1 => b.OwnsOne("Wonderking.Packets.Outgoing.Data.BaseStats", "BaseStats", b1 =>
{ {
@ -284,15 +288,11 @@ namespace Server.DB.Migrations
{ {
b.HasOne("Server.DB.Documents.Character", "Character") b.HasOne("Server.DB.Documents.Character", "Character")
.WithMany() .WithMany()
.HasForeignKey("CharacterId") .HasForeignKey("CharacterId");
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Server.DB.Documents.Guild", "Guild") b.HasOne("Server.DB.Documents.Guild", "Guild")
.WithMany("GuildMembers") .WithMany("GuildMembers")
.HasForeignKey("GuildId") .HasForeignKey("GuildId");
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Character"); b.Navigation("Character");

View file

@ -1,53 +1,16 @@
using JetBrains.Annotations;
namespace Server.DB; namespace Server.DB;
using Documents; using Documents;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
public class WonderkingContext : DbContext public class WonderkingContext : DbContext
{ {
private readonly IConfiguration _configuration; public WonderkingContext([NotNull] DbContextOptions options) : base(options)
private readonly ILoggerFactory _loggerFactory;
public WonderkingContext(ILoggerFactory loggerFactory, IConfiguration configuration)
{ {
this._loggerFactory = loggerFactory;
this._configuration = configuration;
} }
public DbSet<Account> Accounts { get; set; } public DbSet<Account> Accounts { get; set; }
public DbSet<Character> Characters { 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_TieredPGO=1
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
RUN echo "Target: $TARGETARCH" RUN echo "Target: $TARGETARCH" && echo "Build: $BUILDPLATFORM"
RUN echo "Build: $BUILDPLATFORM"
WORKDIR /src WORKDIR /src
COPY ["Wonderking/Wonderking.csproj", "Wonderking/"] COPY ["Wonderking/Wonderking.csproj", "Wonderking/"]
COPY ["Server/Server.csproj", "Server/"] COPY ["Server/Server.csproj", "Server/"]
RUN dotnet restore "Wonderking/Wonderking.csproj" -a $TARGETARCH RUN dotnet restore "Wonderking/Wonderking.csproj" -a $TARGETARCH && dotnet restore "Server/Server.csproj" -a $TARGETARCH
RUN dotnet restore "Server/Server.csproj" -a $TARGETARCH
COPY . . COPY . .
FROM build AS publish FROM build AS publish
RUN dotnet publish "Server/Server.csproj" -c Release -a $TARGETARCH --no-restore -f net8.0 -o /app 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 FROM base AS final
WORKDIR /app WORKDIR /app

View file

@ -1,6 +1,5 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Server.DB.Documents; using Server.DB.Documents;
using Wonderking.Game.Data.Character;
using Wonderking.Packets.Incoming; using Wonderking.Packets.Incoming;
using Wonderking.Packets.Outgoing; using Wonderking.Packets.Outgoing;
using Wonderking.Packets.Outgoing.Data; using Wonderking.Packets.Outgoing.Data;
@ -26,27 +25,23 @@ public class ChannelSelectionHandler : IPacketHandler<ChannelSelectionPacket>
this._wonderkingContext = wonderkingContext; this._wonderkingContext = wonderkingContext;
} }
public ChannelSelectionHandler() public async Task HandleAsync(ChannelSelectionPacket packet, TcpSession session)
{
}
public Task HandleAsync(ChannelSelectionPacket packet, TcpSession session)
{ {
var authSession = (AuthSession)session; var authSession = (AuthSession)session;
ChannelSelectionResponsePacket responsePacket; ChannelSelectionResponsePacket responsePacket;
CharacterSelectionSetGuildNamePacket guildNameResponsePacket; var guildNameResponsePacket = new CharacterSelectionSetGuildNamePacket { GuildNames = Array.Empty<string>() };
var hasCharacters = this._wonderkingContext.Accounts.Include(account => account.Characters) var account = await this._wonderkingContext.Accounts
.FirstOrDefault(a => a.Id == authSession.AccountId)?.Characters.Count > 0; .FirstOrDefaultAsync(a => a.Id == authSession.AccountId).ConfigureAwait(true);
var testingChars = false; if (account != null && account.Characters.Count > 0)
if (hasCharacters && !testingChars)
{ {
responsePacket = new ChannelSelectionResponsePacket responsePacket = new ChannelSelectionResponsePacket
{ {
ChannelIsFullFlag = 0, ChannelIsFullFlag = 0,
Endpoint = "127.0.0.1", Endpoint = "127.0.0.1",
Port = 12345, 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 => .Select(c =>
new CharacterData new CharacterData
{ {
@ -54,127 +49,43 @@ public class ChannelSelectionHandler : IPacketHandler<ChannelSelectionPacket>
Job = c.JobData, Job = c.JobData,
Gender = c.Gender, Gender = c.Gender,
Level = c.Level, 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, Stats = c.BaseStats,
Health = c.Health, Health = c.Health,
Mana = c.Mana, Mana = c.Mana,
EquippedItems = EquippedItems =
c.InventoryItems.Where(item => item.ItemType == ItemType.WornEquipment) c.InventoryItems.Where(item => item.InventoryTab == InventoryTab.WornEquipment)
.Select(item => item.ItemId) .Select(item => item.ItemId)
.ToArray(), .ToArray(),
EquippedCashItems = c.InventoryItems EquippedCashItems = c.InventoryItems
.Where(item => item.ItemType == ItemType.WornCashEquipment) .Where(item => item.InventoryTab == InventoryTab.WornCashEquipment)
.Select(item => item.ItemId) .Select(item => item.ItemId)
.ToArray(), .ToArray()
}) })
.ToArray(), .ToArrayAsync().ConfigureAwait(true),
}; };
guildNameResponsePacket = new CharacterSelectionSetGuildNamePacket guildNameResponsePacket.GuildNames = await _wonderkingContext.Characters
{ .Where(c => c.Account.Id == authSession.AccountId)
GuildNames = this._wonderkingContext.Characters.Where(c => c.AccountId == authSession.AccountId) .Select(character => character.Guild.Name).ToArrayAsync().ConfigureAwait(true);
.Select(character => character.Guild.Name).ToArray()
};
} }
else else
{ {
responsePacket = testingChars responsePacket = new ChannelSelectionResponsePacket
? CreateTestChannelSelectionResponsePacket()
: new ChannelSelectionResponsePacket
{
ChannelIsFullFlag = 0,
Endpoint = "127.0.0.1",
Port = 12345,
Characters = Array.Empty<CharacterData>()
};
guildNameResponsePacket = new CharacterSelectionSetGuildNamePacket
{ {
GuildNames = new[] { "ABCDEFGHIJKLMNOP", "QRSTUVWXYZ123456", "A Guild Name For" } ChannelIsFullFlag = 0,
Endpoint = "127.0.0.1",
Port = 2000,
Characters = Array.Empty<CharacterData>()
}; };
} }
authSession.Send(responsePacket); authSession.Send(responsePacket);
if (guildNameResponsePacket.GuildNames.Length > 0) if (guildNameResponsePacket.GuildNames.Length > 0 &&
guildNameResponsePacket.GuildNames.Select(n => n != string.Empty).Any())
{ {
authSession.Send(guildNameResponsePacket); 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 var loginResponsePacket = new LoginResponsePacket
{ {
ResponseReason = loginResponseReason, ResponseReason = loginResponseReason,
ChannelData = new[] { new ServerChannelData { ChannelId = 0, LoadPercentage = 75, ServerId = 0 } }, ChannelData = new[] { new ServerChannelData { ChannelId = 0, LoadPercentage = 0, ServerId = 0 } },
UnknownFlag = 1, UnknownFlag = 1,
IsGameMaster = true IsGameMaster = true
}; };

View file

@ -1,5 +1,6 @@
using System.Net; using System.Net;
using System.Reflection; using System.Reflection;
using System.Text.Json;
using MassTransit; using MassTransit;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
@ -8,23 +9,42 @@ using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Server.DB; using Server.DB;
using Server.Services; using Server.Services;
using Wonderking.Game.Mapping;
var builder = Host.CreateApplicationBuilder(); var builder = Host.CreateApplicationBuilder();
#if DEBUG #if DEBUG
builder.Environment.EnvironmentName = "Development"; builder.Environment.EnvironmentName = "Development";
#endif #endif
builder.Configuration.AddJsonFile("settings.json", true, true) builder.Configuration.AddJsonFile("settings.json", true, true)
.AddJsonFile($"settings.{builder.Environment.EnvironmentName}.json", true) .AddJsonFile($"settings.{builder.Environment.EnvironmentName}.json", true)
.AddEnvironmentVariables().Build(); .AddEnvironmentVariables().Build();
builder.Services.AddSingleton<CharacterStatsMappingConfiguration>(
JsonSerializer.Deserialize<CharacterStatsMappingConfiguration>(
File.ReadAllText("config/character-stats.mapping.json")) ?? throw new InvalidOperationException());
builder.Services.AddLogging(); builder.Services.AddLogging();
builder.Logging.AddFile("Logs/Server-{Date}.log", LogLevel.Trace); var loggerFactory = LoggerFactory.Create(loggingBuilder =>
builder.Logging.AddFile("Logs/Server-{Date}.json.log", LogLevel.Trace, isJson: true); {
builder.Services.AddEntityFrameworkNpgsql(); loggingBuilder.AddFile("logs/Server-{Date}.log", LogLevel.Trace);
builder.Services.AddDbContext<WonderkingContext>(); 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<PacketDistributorService>();
builder.Services.AddSingleton<ItemObjectPoolService>();
builder.Services.AddHostedService<ItemObjectPoolService>();
builder.Services.AddHostedService(provider => builder.Services.AddHostedService(provider =>
provider.GetService<PacketDistributorService>() ?? throw new InvalidOperationException()); provider.GetService<PacketDistributorService>() ?? throw new InvalidOperationException());
builder.Services.AddSingleton<ItemObjectPoolService>();
builder.Services.AddMassTransit(x => builder.Services.AddMassTransit(x =>
{ {
x.UsingInMemory((context, configurator) => configurator.ConfigureEndpoints(context)); 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" Version="8.0.0"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Abstractions" 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.Analyzers" Version="8.0.0"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.0"> <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.0">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View file

@ -1,6 +1,8 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Server.DB.Documents;
using Wonderking.Game.Data; using Wonderking.Game.Data;
using Wonderking.Game.Reader; using Wonderking.Game.Reader;
@ -8,35 +10,48 @@ namespace Server.Services;
public class ItemObjectPoolService : IHostedService public class ItemObjectPoolService : IHostedService
{ {
readonly ConcurrentDictionary<uint, ItemObject> _itemObjectPool = new(); readonly ConcurrentDictionary<uint, ItemObject> _itemObjectPool;
private readonly ItemReader _itemReader; 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") ?? _itemReader = new ItemReader(configuration.GetSection("Game").GetSection("Data").GetValue<string>("Path") ??
string.Empty); string.Empty);
_itemObjectPool = new ConcurrentDictionary<uint, ItemObject>();
} }
public Task StartAsync(CancellationToken cancellationToken) public Task StartAsync(CancellationToken cancellationToken)
{ {
var amountOfEntries = _itemReader.GetAmountOfEntries(); var amountOfEntries = _itemReader.GetAmountOfEntries();
ParallelEnumerable.Range(0, (int)amountOfEntries).AsParallel().ForAll(i => Parallel.For(0, (int)amountOfEntries, i =>
{ {
var itemObject = _itemReader.GetEntry((uint)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; return Task.CompletedTask;
} }
public Task StopAsync(CancellationToken cancellationToken) public Task StopAsync(CancellationToken cancellationToken)
{ {
_itemReader.Dispose();
return Task.CompletedTask; return Task.CompletedTask;
} }
public ItemObject GetItem(ushort itemId) public ItemObject GetItem(ushort itemId)
{ {
return _itemObjectPool[itemId]; _ = _itemObjectPool.TryGetValue(itemId, out var itemObject);
return itemObject;
} }
public bool ContainsItem(ushort itemId) public bool ContainsItem(ushort itemId)
@ -48,4 +63,24 @@ public class ItemObjectPoolService : IHostedService
{ {
return _itemObjectPool.AsReadOnly().Values.AsQueryable(); 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 ConcurrentQueue<RawPacket> _concurrentQueue;
private readonly private ImmutableDictionary<OperationCode,
ImmutableDictionary<OperationCode, Func<byte[], IPacket>> _deserializationMap;
Func<byte[], IPacket>> _deserializationMap;
private readonly ILogger<PacketDistributorService> _logger; 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) public PacketDistributorService(ILogger<PacketDistributorService> logger, IServiceProvider serviceProvider)
{ {
this._concurrentQueue = new ConcurrentQueue<RawPacket>(); this._concurrentQueue = new ConcurrentQueue<RawPacket>();
this._logger = logger; this._logger = logger;
_serviceProvider = serviceProvider;
}
public Task StartAsync(CancellationToken cancellationToken)
{
var tempDeserializationMap = var tempDeserializationMap =
new Dictionary<OperationCode, Func<byte[], IPacket>>(); new Dictionary<OperationCode, Func<byte[], IPacket>>();
@ -43,7 +48,7 @@ public class PacketDistributorService : IHostedService
packetHandlers.ForEach(x => packetHandlers.ForEach(x =>
{ {
var packetHandler = var packetHandler =
ActivatorUtilities.GetServiceOrCreateInstance(serviceProvider, ActivatorUtilities.GetServiceOrCreateInstance(_serviceProvider,
x.Value); x.Value);
this._packetHandlersInstantiation.TryAdd(x.Key, packetHandler); this._packetHandlersInstantiation.TryAdd(x.Key, packetHandler);
}); });
@ -60,15 +65,14 @@ public class PacketDistributorService : IHostedService
Return(packetVariable); Return(packetVariable);
}).Compile(); }).Compile();
logger.PacketCreationFunctionCreated(packetsType.Key); _logger.PacketCreationFunctionCreated(packetsType.Key);
tempDeserializationMap.Add(packetsType.Key, lambda); tempDeserializationMap.Add(packetsType.Key, lambda);
} }
this._deserializationMap = tempDeserializationMap.ToImmutableDictionary(); this._deserializationMap = tempDeserializationMap.ToImmutableDictionary();
return Task.CompletedTask;
} }
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
private Dictionary<OperationCode, Type> GetPacketsWithId(Assembly executingAssembly) private Dictionary<OperationCode, Type> GetPacketsWithId(Assembly executingAssembly)

View file

@ -58,5 +58,17 @@ public class WonderkingAuthServer : TcpServer, IHostedService
base.OnStopped(); 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); protected override void OnError(SocketError error) => this._logger.LogError("An error has occured {Error}", error);
} }

View file

@ -1,44 +1,52 @@
services: services:
server: server:
container_name: continuity-server container_name: continuity-server
image: continuity:latest image: continuity:latest
restart: always restart: always
depends_on: depends_on:
- db - db
environment: environment:
- ENVIRONMENT=Development - ENVIRONMENT=Development
- Testing:CreateAccountOnLogin=true - Testing:CreateAccountOnLogin=true
- DB:Host=db - DB:Host=db
- DB:Port=5432 - DB:Port=5432
- DB:Username=continuity - DB:Username=continuity
- DB:Password=continuity - DB:Password=continuity
- Game:Data:Path=/app/data - Game:Data:Path=/app/data/
networks: networks:
- continuity - continuity
ports: ports:
- "10001:10001" - 10001:10001
volumes: volumes:
- type: bind - type: bind
source: game-data source: ../wk-data
target: /app/data target: /app/data
read_only: true 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: db:
container_name: continuity-db container_name: continuity-db
image: postgres:16.1-alpine image: postgres:16.1-alpine
restart: always restart: always
environment: environment:
- POSTGRES_USER=continuity - POSTGRES_USER=continuity
- POSTGRES_DB=continuity - POSTGRES_DB=continuity
- POSTGRES_PASSWORD=continuity - POSTGRES_PASSWORD=continuity
networks: networks:
- continuity - continuity
ports: ports:
- "5432:5432" - 5432:5432
volumes: volumes:
- db-data:/var/lib/postgresql/data - db-data:/var/lib/postgresql/data
healthcheck: 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 interval: 10s
timeout: 3s timeout: 3s
retries: 3 retries: 3

View file

@ -11,7 +11,7 @@
}, },
"Game":{ "Game":{
"Data":{ "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="Login-Response.md">Login Response</a></li>
<li><a href="Channel-Selection.md">Channel Selection</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="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> </list>

View file

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

View file

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

View file

@ -7,7 +7,10 @@ public abstract class DataReader<T>
protected DataReader(string path) protected DataReader(string path)
{ {
Path = path; Path = path;
DatFileContent = new(GetDatFileContent(path).ToArray()); _xorKey = GetXorKey();
SizeOfEntry = GetSizeOfEntry();
_datFileName = GetDatFileName();
DatFileContent = GetDatFileContent(path).ToArray();
} }
private protected string Path { get; init; } private protected string Path { get; init; }
@ -15,7 +18,7 @@ public abstract class DataReader<T>
public abstract uint GetAmountOfEntries(); public abstract uint GetAmountOfEntries();
public abstract T GetEntry(uint entryId); public abstract T GetEntry(uint entryId);
protected ushort GetSizeOfEntry() private static ushort GetSizeOfEntry()
{ {
return typeof(T).GetCustomAttribute<GameDataMetadataAttribute>()?.DataEntrySize ?? return typeof(T).GetCustomAttribute<GameDataMetadataAttribute>()?.DataEntrySize ??
throw new NotSupportedException("DataEntrySize is null"); throw new NotSupportedException("DataEntrySize is null");
@ -33,16 +36,20 @@ public abstract class DataReader<T>
throw new NotSupportedException("XorKey is null"); 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]; var data = new byte[fileData.Length];
for (var i = 0; i < fileData.Length; i++) for (var i = 0; i < fileData.Length; i++)
{ {
data[i] = (byte)(fileData[i] ^ GetXorKey()); data[i] = (byte)(fileData[i] ^ this._xorKey);
} }
return data; 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;
using Wonderking.Game.Data.Item; using Wonderking.Game.Data.Item;
namespace Wonderking.Game.Reader; 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() 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) public override ItemObject GetEntry(uint entryId)
{ {
var item = new ItemObject(); var item = new ItemObject();
this.DatFileContent.Position = 9 + entryId * this.GetSizeOfEntry(); var arraySegment = new ArraySegment<byte>(DatFileContent,
var reader = new BinaryReader(this.DatFileContent); 9 + (int)entryId * this.SizeOfEntry, this.SizeOfEntry);
item.ItemID = reader.ReadUInt32(); //9 var data = new Span<byte>(arraySegment.Array, arraySegment.Offset, arraySegment.Count);
item.Disabled = reader.ReadUInt32() == 1; //13 item.ItemID = BitConverter.ToUInt32(data.Slice(0, 4)); // 0 -> 4
item.ItemType = reader.ReadUInt32(); //17 item.Disabled = BitConverter.ToBoolean(data.Slice(4, 4)); // 4 -> 8
item.Unknown2 = reader.ReadBytes(4); //21 item.ItemType = BitConverter.ToUInt32(data.Slice(8, 4)); // 8 -> 12
item.Unknown3 = reader.ReadBytes(4); //25 item.Unknown2 = data.Slice(12, 4).ToArray(); // 12 -> 16
item.ClassNo1 = reader.ReadUInt32(); //29 item.Unknown3 = data.Slice(16, 4).ToArray(); // 16 -> 20
item.ClassNo2 = reader.ReadUInt32(); //33 item.ClassNo1 = BitConverter.ToUInt32(data.Slice(20, 4)); // 20 -> 24
item.ClassNo3 = reader.ReadUInt32(); //37 item.ClassNo2 = BitConverter.ToUInt32(data.Slice(24, 4)); // 24 -> 28
item.ClassNo4 = reader.ReadUInt32(); //41 item.ClassNo3 = BitConverter.ToUInt32(data.Slice(28, 4)); // 28 -> 32
item.SlotNo1 = reader.ReadUInt32(); //45 item.ClassNo4 = BitConverter.ToUInt32(data.Slice(32, 4)); // 32 -> 36
item.SlotNo2 = reader.ReadUInt32(); //49 item.SlotNo1 = BitConverter.ToUInt32(data.Slice(36, 4)); // 36 -> 40
item.Unknown4 = reader.ReadBytes(4); //53 item.SlotNo2 = BitConverter.ToUInt32(data.Slice(40, 4)); // 40 -> 44
item.IsCash = reader.ReadUInt32(); //57 item.Unknown4 = data.Slice(44, 4).ToArray(); // 44 -> 48
item.Unknown5 = reader.ReadBytes(4); //61 item.IsCash = BitConverter.ToUInt32(data.Slice(48, 4)); // 48 -> 52
item.Price = reader.ReadUInt32(); //65 item.Unknown5 = data.Slice(52, 4).ToArray(); // 52 -> 56
item.Unknown7 = reader.ReadBytes(4); //69 item.Price = BitConverter.ToUInt32(data.Slice(56, 4)); // 56 -> 60
item.MaxNumber = reader.ReadUInt32(); //73 item.Unknown7 = data.Slice(60, 4).ToArray(); // 60 -> 64
item.Unknown17 = reader.ReadBytes(12); //77 item.MaxNumber = BitConverter.ToUInt32(data.Slice(64, 4)); // 64 -> 68
item.MaximumLevelRequirement = reader.ReadUInt32(); //89 item.Unknown17 = data.Slice(68, 12).ToArray(); // 68 -> 80
item.SexNo = reader.ReadUInt32(); //93 item.MaximumLevelRequirement = BitConverter.ToUInt32(data.Slice(80, 4)); // 80 -> 84
item.WeaponSomething = reader.ReadUInt32(); //97 item.SexNo = BitConverter.ToUInt32(data.Slice(84, 4)); // 84 -> 88
item.Unknown8 = reader.ReadBytes(4); //101 item.WeaponSomething = BitConverter.ToUInt32(data.Slice(88, 4)); // 88 -> 92
item.R2C = reader.ReadBytes(16); //105 item.Unknown8 = data.Slice(92, 4).ToArray(); // 92 -> 96
item.Unknown9 = reader.ReadBytes(4); //121 item.R2C = data.Slice(96, 16).ToArray(); // 96 -> 112
item.Stats = reader.ReadStats(); //125 item.Unknown9 = data.Slice(112, 4).ToArray(); // 112 -> 116
item.ElementalStats = reader.ReadElementalStats(); //149 item.Stats = new Stats
item.R7C = reader.ReadBytes(4); //213 {
item.R8C = reader.ReadBytes(8); //217 Strength = BitConverter.ToInt32(data.Slice(116, 4)), // 116 -> 120
item.Speed = reader.ReadSingle(); //225 Dexterity = BitConverter.ToInt32(data.Slice(120, 4)), // 120 -> 124
item.Jump = reader.ReadSingle(); //229 Intelligence = BitConverter.ToInt32(data.Slice(124, 4)), // 124 -> 128
item.StatDefense = reader.ReadInt32(); //233 Vitality = BitConverter.ToInt32(data.Slice(128, 4)), // 128 -> 132
item.MagicID = reader.ReadUInt32(); //237 Luck = BitConverter.ToInt32(data.Slice(132, 4)), // 132 -> 136
item.Unknown13 = reader.ReadBytes(4); //241 Wisdom = BitConverter.ToInt32(data.Slice(136, 4)), // 136 -> 140
item.Unknown14 = reader.ReadBytes(4); //245 }; // 116 -> 140
item.AdditionalHealthRecoveryVolume = reader.ReadInt32(); //249 item.ElementalStats = new ElementalStats
item.R9C_1 = reader.ReadBytes(4); //253 {
item.AdditionalManaRecoveryVolume = reader.ReadInt32(); //257 MinimumFireDamage = BitConverter.ToInt32(data.Slice(140, 4)), // 140 -> 144
item.R9C_2 = reader.ReadBytes(4); //261 MinimumWaterDamage = BitConverter.ToInt32(data.Slice(144, 4)), // 144 -> 148
item.R10C = reader.ReadBytes(8); //265 MinimumDarkDamage = BitConverter.ToInt32(data.Slice(148, 4)), // 148 -> 152
item.AdditionalHealthPoints = reader.ReadInt32(); //273 MinimumHolyDamage = BitConverter.ToInt32(data.Slice(152, 4)), // 152 -> 156
item.AdditionalManaPoints = reader.ReadInt32(); //277 MaximumFireDamage = BitConverter.ToInt32(data.Slice(156, 4)), // 156 -> 160
item.IsArrow = reader.ReadBoolean(); //281 MaximumWaterDamage = BitConverter.ToInt32(data.Slice(160, 4)), // 160 -> 164
item.Unknown18 = reader.ReadBytes(7); //282 MaximumDarkDamage = BitConverter.ToInt32(data.Slice(164, 4)), // 164 -> 168
item.AdditionalEvasionRate = reader.ReadInt32(); //289 MaximumHolyDamage = BitConverter.ToInt32(data.Slice(168, 4)), // 168 -> 172
item.HitRate = reader.ReadInt32(); //293 ElementFire = BitConverter.ToUInt32(data.Slice(172, 4)), // 172 -> 176
item.ChanceToHit = reader.ReadInt32(); //297 ElementWater = BitConverter.ToUInt32(data.Slice(176, 4)), // 176 -> 180
item.MagicalDamage = reader.ReadInt32(); //301 ElementDark = BitConverter.ToUInt32(data.Slice(180, 4)), // 180 -> 184
item.CriticalHitChance = reader.ReadInt32(); //305 ElementHoly = BitConverter.ToUInt32(data.Slice(184, 4)), // 184 -> 188
item.R12C = reader.ReadBytes(4); //309 FireResistance = BitConverter.ToInt32(data.Slice(188, 4)), // 188 -> 192
item.Unknown16 = reader.ReadBytes(4); //313 WaterResistance = BitConverter.ToInt32(data.Slice(192, 4)), // 192 -> 196
item.MinimalAttackDamage = reader.ReadInt32(); //317 DarkResistance = BitConverter.ToInt32(data.Slice(196, 4)), // 196 -> 200
item.MaximalAttackDamage = reader.ReadInt32(); //321 HolyResistance = BitConverter.ToInt32(data.Slice(200, 4)), // 200 -> 204
item.PhysicalDamage = reader.ReadInt32(); //325 }; // 140 -> 204
item.CraftMaterial = reader.ReadCraftMaterial(); //329 item.R7C = data.Slice(204, 4).ToArray(); // 204 -> 208
item.CraftResultAmount = reader.ReadUInt32(); //361 item.R8C = data.Slice(208, 8).ToArray(); // 208 -> 216
item.R14C = reader.ReadBytes(4); //365 item.Speed = BinaryPrimitives.ReadSingleLittleEndian(data.Slice(216, 4)); // 216 -> 220
item.CraftResultItem = reader.ReadUInt32(); //369 item.Jump = BinaryPrimitives.ReadSingleLittleEndian(data.Slice(220, 4)); // 220 -> 224
item.R15C = reader.ReadBytes(4); //373 item.StatDefense = BitConverter.ToInt32(data.Slice(224, 4)); // 224 -> 228
item.R16C = reader.ReadBytes(20); //377 item.MagicID = BitConverter.ToUInt32(data.Slice(228, 4)); // 228 -> 232
item.InventoryX = reader.ReadInt32(); //397 item.Unknown13 = data.Slice(232, 4).ToArray(); // 232 -> 236
item.InventoryY = reader.ReadInt32(); //401 item.Unknown14 = data.Slice(236, 4).ToArray(); // 236 -> 240
item.InventoryWidth = reader.ReadInt32(); //405 item.AdditionalHealthRecoveryVolume = BitConverter.ToInt32(data.Slice(240, 4)); // 240 -> 244
item.InventoryHeight = reader.ReadInt32(); //409 item.R9C_1 = data.Slice(244, 4).ToArray(); // 244 -> 248
item.SheetID = reader.ReadInt32(); //413 item.AdditionalManaRecoveryVolume = BitConverter.ToInt32(data.Slice(248, 4)); // 248 -> 252
item.Name = reader.ReadString(20); //417 item.R9C_2 = data.Slice(252, 4).ToArray(); // 252 -> 256
item.Description = reader.ReadString(85); //427 item.R10C = data.Slice(256, 8).ToArray(); // 256 -> 264
item.Unknown1 = reader.ReadBytes(175); //493 item.AdditionalHealthPoints = BitConverter.ToInt32(data.Slice(264, 4)); // 264 -> 268
item.IsEnchantable = reader.ReadUInt32() == 1; //687 item.AdditionalManaPoints = BitConverter.ToInt32(data.Slice(268, 4)); // 268 -> 272
item.Unknown1_2 = reader.ReadBytes(104); //687 item.IsArrow = BitConverter.ToBoolean(data.Slice(272, 1)); // 272 -> 273
item.SetItems = reader.ReadArray<uint>(5); item.Unknown18 = data.Slice(273, 7).ToArray(); // 273 -> 280
item.SetID = reader.ReadUInt32(); //691 item.AdditionalEvasionRate = BitConverter.ToInt32(data.Slice(280, 4)); // 280 -> 284
item.Options = reader.ReadItemOptions(); //819 item.HitRate = BitConverter.ToInt32(data.Slice(284, 4)); // 284 -> 288
item.Unknown19 = reader.ReadBytes(23); //835 item.ChanceToHit = BitConverter.ToInt32(data.Slice(288, 4)); // 288 -> 292
item.PetID = reader.ReadByte(); //858 item.MagicalDamage = BitConverter.ToInt32(data.Slice(292, 4)); // 292 -> 296
item.Unknown20 = reader.ReadBytes(20); //859 item.CriticalHitChance = BitConverter.ToInt32(data.Slice(296, 4)); // 296 -> 300
item.HitBoxScaling = reader.ReadByte(); //879 item.R12C = data.Slice(300, 4).ToArray(); // 300 -> 304
item.Unknown20_2 = reader.ReadBytes(13); //880 item.Unknown16 = data.Slice(304, 4).ToArray(); // 304 -> 308
item.ContainedItems = reader.ReadContainedItems(); //893 item.MinimalAttackDamage = BitConverter.ToInt32(data.Slice(308, 4)); // 308 -> 312
item.IsQuestItem = reader.ReadBoolean(); //923 item.MaximalAttackDamage = BitConverter.ToInt32(data.Slice(312, 4)); // 312 -> 316
item.MinimumLevelRequirement = reader.ReadByte(); //924 item.PhysicalDamage = BitConverter.ToInt32(data.Slice(316, 4)); // 316 -> 320
item.Unknown21_2 = reader.ReadBytes(6); //925 item.CraftMaterial = new CraftMaterial[]
reader.Dispose(); //931 {
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; return item;
} }
public void Dispose()
{
this.DatFileContent.Dispose();
}
} }

View file

@ -82,14 +82,14 @@ public static class ItemReaderExtensions
options.OptionAvailable = reader.ReadInt32() == 1; //819 options.OptionAvailable = reader.ReadInt32() == 1; //819
var optionIDs = new List<byte>(4); var optionIDs = new List<uint>(4);
//823 //823
for (var i = 0; i < 3; i++) 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; 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, LoginResponse = 12,
ChannelSelection = 13, ChannelSelection = 13,
ChannelSelectionResponse = 13, ChannelSelectionResponse = 13,
CharacterNameCheck = 14,
CharacterNameCheckResponse = 14,
CharacterCreation = 15,
CharacterCreationResponse = 15,
CharacterDeletion = 16,
CharacterDeletionResponse = 16,
CharacterSelection = 17,
CharacterSelectionSetGuildName = 19, 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 JetBrains.Annotations;
using Microsoft.EntityFrameworkCore;
namespace Wonderking.Packets.Outgoing.Data; namespace Wonderking.Packets.Outgoing.Data;
[UsedImplicitly] [UsedImplicitly]
[Owned]
public class BaseStats public class BaseStats
{ {
public required short Strength { get; set; } [JsonPropertyName("strength")] public required short Strength { get; set; }
public required short Dexterity { get; set; } [JsonPropertyName("dexterity")] public required short Dexterity { get; set; }
public required short Intelligence { get; set; } [JsonPropertyName("intelligence")] public required short Intelligence { get; set; }
public required short Vitality { get; set; } [JsonPropertyName("vitality")] public required short Vitality { get; set; }
public required short Luck { get; set; } [JsonPropertyName("luck")] public required short Luck { get; set; }
public required short Wisdom { get; set; } [JsonPropertyName("wisdom")] public required short Wisdom { get; set; }
} }

View file

@ -1,8 +1,10 @@
using JetBrains.Annotations; using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore;
namespace Wonderking.Packets.Outgoing.Data; namespace Wonderking.Packets.Outgoing.Data;
[UsedImplicitly] [UsedImplicitly]
[Owned]
public class JobData public class JobData
{ {
public required byte FirstJob { get; set; } public required byte FirstJob { get; set; }

View file

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

View file

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