bugfix/87-client-dc-on-char-deletion #90

Merged
rainote merged 12 commits from bugfix/87-client-dc-on-char-deletion into master 2023-11-19 14:10:19 +00:00
14 changed files with 122 additions and 56 deletions

View file

@ -0,0 +1,16 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Server/Dockerfile" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
<deployment type="dockerfile">
<settings>
<option name="imageTag" value="continuity" />
<option name="buildCliOptions" value="--platform linux/amd64" />
<option name="buildKitEnabled" value="true" />
<option name="buildOnly" value="true" />
<option name="contextFolderPath" value="." />
<option name="sourceFilePath" value="Server/Dockerfile" />
</settings>
</deployment>
<EXTENSION ID="com.jetbrains.rider.docker.debug" isFastModeEnabled="true" isSslEnabled="false" />
<method v="2" />
</configuration>
</component>

View file

@ -1,2 +1,6 @@
# What is this project about?
This is a continuation and rewrite of the original Project Infinity for Wonderking.
## Important notes for developers
* Avoid using statements with TcpSession, AuthSession or any type that inherits Session outside the session itself.

View file

@ -29,14 +29,14 @@ public class AuthSession : TcpSession
return base.Send(buffer);
}
public void Send(IPacket packet)
public Task SendAsync(IPacket packet)
{
var type = packet.GetType();
this._logger.LogInformation("Packet of type {Type} is being serialized", type.Name);
var packetIdAttribute = type.GetCustomAttribute<PacketIdAttribute>();
if (packetIdAttribute == null)
{
return;
return Task.CompletedTask;
}
var opcode = packetIdAttribute.Code;
@ -63,7 +63,8 @@ public class AuthSession : TcpSession
this._logger.LogInformation("Packet data being parsed is: {Data}", BitConverter.ToString(packetData.ToArray()));
this._logger.LogInformation("Packet being parsed is: {Data}", BitConverter.ToString(buffer.ToArray()));
this.Send(buffer);
this.SendAsync(buffer);
return Task.CompletedTask;
}
protected override void OnReceived(byte[] buffer, long offset, long size)

View file

@ -39,7 +39,7 @@ public class ChannelSelectionHandler : IPacketHandler<ChannelSelectionPacket>
{
ChannelIsFullFlag = 0,
Endpoint = "127.0.0.1",
Port = 12345,
Port = 2000,
Characters = await _wonderkingContext.Characters.AsNoTracking()
.Where(c => c.Account.Id == authSession.AccountId)
.Select(c =>
@ -54,20 +54,19 @@ public class ChannelSelectionHandler : IPacketHandler<ChannelSelectionPacket>
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
EquippedItems = GetItemIDsByInventoryTab(c.InventoryItems
.Where(item => item.InventoryTab == InventoryTab.WornEquipment)
.Select(item => new Tuple<ushort, byte>(item.ItemId, item.Slot)).ToArray()),
EquippedCashItems = GetItemIDsByInventoryTab(c.InventoryItems
.Where(item => item.InventoryTab == InventoryTab.WornCashEquipment)
.Select(item => item.ItemId)
.ToArray()
.Select(item => new Tuple<ushort, byte>(item.ItemId, item.Slot)).ToArray()),
})
.ToArrayAsync().ConfigureAwait(true),
};
guildNameResponsePacket.GuildNames = await _wonderkingContext.Characters
.Where(c => c.Account.Id == authSession.AccountId)
.Where(c => c.Guild != null)
.Select(character => character.Guild.Name).ToArrayAsync().ConfigureAwait(true);
}
else
@ -81,11 +80,23 @@ public class ChannelSelectionHandler : IPacketHandler<ChannelSelectionPacket>
};
}
authSession.Send(responsePacket);
await authSession.SendAsync(responsePacket).ConfigureAwait(false);
if (guildNameResponsePacket.GuildNames.Length > 0 &&
guildNameResponsePacket.GuildNames.Select(n => n != string.Empty).Any())
{
authSession.Send(guildNameResponsePacket);
await authSession.SendAsync(guildNameResponsePacket).ConfigureAwait(false);
}
}
private static ushort[] GetItemIDsByInventoryTab(Tuple<ushort, byte>[] items)
{
var ids = new ushort[20];
for (var i = 0; i < 20; i++)
{
ids[i] = items.FirstOrDefault(item => item.Item2 == i)?.Item1 ?? 0;
}
return ids;
}
}

View file

@ -28,8 +28,13 @@ public class CharacterCreationHandler : IPacketHandler<CharacterCreationPacket>
public async Task HandleAsync(CharacterCreationPacket packet, TcpSession session)
{
var authSession = session as AuthSession;
if (authSession is null)
{
return;
}
var account =
_wonderkingContext.Accounts.FirstOrDefault(a => authSession != null && a.Id == authSession.AccountId);
_wonderkingContext.Accounts.FirstOrDefault(a => a.Id == authSession.AccountId);
var mappedDefaultItems = _characterStatsMapping.DefaultCharacterMapping.Items
.Select(i => _itemObjectPoolService.GetBaseInventoryItem(i.Id, i.Quantity)).ToArray();
@ -83,10 +88,10 @@ public class CharacterCreationHandler : IPacketHandler<CharacterCreationPacket>
await _wonderkingContext.SaveChangesAsync().ConfigureAwait(true);
var amountOfCharacters = await _wonderkingContext.Characters.AsNoTrackingWithIdentityResolution()
.CountAsync(c => authSession != null && c.Account.Id == authSession.AccountId).ConfigureAwait(true);
.CountAsync(c => 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)
.Where(c => c.Account.Id == authSession.AccountId && c.Name == packet.Name)
.Select(c =>
new CharacterData
{
@ -99,20 +104,19 @@ public class CharacterCreationHandler : IPacketHandler<CharacterCreationPacket>
Health = c.Health,
Mana = c.Mana,
EquippedItems =
c.InventoryItems.Where(item => item.InventoryTab == InventoryTab.WornEquipment)
.Select(item => item.ItemId)
.ToArray(),
EquippedCashItems = c.InventoryItems
GetItemIDsByInventoryTab(c.InventoryItems
.Where(item => item.InventoryTab == InventoryTab.WornEquipment)
.Select(item => new Tuple<ushort, byte>(item.ItemId, item.Slot)).ToArray()),
EquippedCashItems = GetItemIDsByInventoryTab(c.InventoryItems
.Where(item => item.InventoryTab == InventoryTab.WornCashEquipment)
.Select(item => item.ItemId)
.ToArray(),
.Select(item => new Tuple<ushort, byte>(item.ItemId, item.Slot)).ToArray()),
}).FirstAsync().ConfigureAwait(true);
authSession?.Send(new CharacterCreationResponsePacket
await authSession.SendAsync(new CharacterCreationResponsePacket
{
Character = character,
Slot = amountOfCharacters - 1,
Slot = packet.Slot,
isDuplicate = false,
});
}).ConfigureAwait(false);
}
private static int CalculateCurrentHealth(ushort level, JobSpecificMapping firstJobConfig)
@ -126,4 +130,16 @@ public class CharacterCreationHandler : IPacketHandler<CharacterCreationPacket>
return (int)((level - 1) * firstJobConfig.DynamicStats.ManaPerLevel +
firstJobConfig.BaseStats.Wisdom * firstJobConfig.DynamicStats.ManaPerWisdom);
}
private static ushort[] GetItemIDsByInventoryTab(Tuple<ushort, byte>[] items)
{
var ids = new ushort[20];
for (var i = 0; i < 20; i++)
{
ids[i] = items.FirstOrDefault(item => item.Item2 == i)?.Item1 ?? 0;
}
return ids;
}
}

View file

@ -17,8 +17,7 @@ public class CharacterDeletionHandler : IPacketHandler<CharacterDeletePacket>
public async Task HandleAsync(CharacterDeletePacket packet, TcpSession session)
{
using var authSession = session as AuthSession;
if (authSession == null)
if (session is not AuthSession authSession)
{
session.Disconnect();
return;
@ -30,13 +29,13 @@ public class CharacterDeletionHandler : IPacketHandler<CharacterDeletePacket>
var response = new CharacterDeleteResponsePacket { IsDeleted = 0 };
if (character == null)
{
authSession.Send(response);
await authSession.SendAsync(response).ConfigureAwait(false);
return;
}
_wonderkingContext.Characters.Remove(character);
await _wonderkingContext.SaveChangesAsync().ConfigureAwait(false);
authSession.Send(response);
await authSession.SendAsync(response).ConfigureAwait(false);
}
}

View file

@ -18,8 +18,11 @@ public class CharacterNameCheckHandler : IPacketHandler<CharacterNameCheckPacket
{
var isTaken = _wonderkingContext.Characters.Any(c => c.Name == packet.Name);
var responsePacket = new CharacterNameCheckPacketResponse { IsTaken = isTaken };
var authSession = session as AuthSession;
authSession?.Send(responsePacket);
if (session is AuthSession authSession)
{
return authSession.SendAsync(responsePacket);
}
return Task.CompletedTask;
}
}

View file

@ -119,6 +119,6 @@ public class LoginHandler : IPacketHandler<LoginInfoPacket>
}
_logger.LogInformation("LoginResponsePacket: {@LoginResponsePacket}", loginResponsePacket);
sess?.Send(loginResponsePacket);
sess?.SendAsync(loginResponsePacket);
}
}

View file

@ -42,7 +42,8 @@ builder.Services.AddDbContextPool<WonderkingContext>(o =>
builder.Services.AddSingleton<ILoggerFactory>(loggerFactory);
builder.Services.AddSingleton<PacketDistributorService>();
builder.Services.AddSingleton<ItemObjectPoolService>();
builder.Services.AddHostedService<ItemObjectPoolService>();
builder.Services.AddHostedService(provider =>
provider.GetService<ItemObjectPoolService>() ?? throw new InvalidOperationException());
builder.Services.AddHostedService(provider =>
provider.GetService<PacketDistributorService>() ?? throw new InvalidOperationException());
builder.Services.AddMassTransit(x =>

View file

@ -8,7 +8,6 @@
<RootNamespace>Server</RootNamespace>
<LangVersion>default</LangVersion>
<ServerGarbageCollection>true</ServerGarbageCollection>
<TargetFramework>net8.0</TargetFramework>
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<Features>strict</Features>
<Authors>Timothy (RaiNote) Schenk</Authors>
@ -20,6 +19,7 @@
<ProduceReferenceAssembly>True</ProduceReferenceAssembly>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<AnalysisLevel>latest-recommended</AnalysisLevel>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0|AnyCPU'">
@ -82,7 +82,7 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="NetCoreServer" Version="7.0.0"/>
<PackageReference Include="NetCoreServer" Version="7.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3"/>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0-rc.2" />
<PackageReference Include="Nullable.Extended.Analyzer" Version="1.10.4539">

View file

@ -71,8 +71,18 @@ public class ItemObjectPoolService : IHostedService
{
ItemId = itemId,
Count = count,
Slot = 0,
InventoryTab = InventoryTab.WornEquipment,
Slot = (byte)item.SlotNo1,
InventoryTab =
item.ItemType switch
{
1 => InventoryTab.WornCashEquipment,
2 => isWorn ? InventoryTab.WornEquipment : InventoryTab.Equipment,
3 => InventoryTab.Etc,
4 => isWorn ? InventoryTab.WornCashEquipment : InventoryTab.Cash,
5 => InventoryTab.Warehouse,
0 => InventoryTab.WornEquipment,
_ => 0
},
Level = item.MinimumLevelRequirement,
Rarity = 0,
AddOption = 0,

View file

@ -27,6 +27,7 @@ public class WonderkingAuthServer : TcpServer, IHostedService
public Task StopAsync(CancellationToken cancellationToken)
{
this.DisconnectAll();
this.Stop();
return Task.CompletedTask;
}

View file

@ -29,39 +29,41 @@ public class ChannelSelectionResponsePacket : IPacket
// Character Data
for (var i = 0; i < Characters.Length; i++)
{
int offset = 20 + (i * 132);
var character = Characters[i];
BinaryPrimitives.WriteInt32LittleEndian(data.Slice(20 + (i * 132), 4), i);
Encoding.ASCII.GetBytes(character.Name, data.Slice(24 + (i * 132), 20));
// Character Data
BinaryPrimitives.WriteInt32LittleEndian(data.Slice(offset, 4), i);
Encoding.ASCII.GetBytes(character.Name, data.Slice(offset + 4, 20));
// Job Data
data[44 + (i * 132)] = character.Job.FirstJob;
data[45 + (i * 132)] = character.Job.SecondJob;
data[46 + (i * 132)] = character.Job.ThirdJob;
data[47 + (i * 132)] = character.Job.FourthJob;
data[offset + 24] = character.Job.FirstJob;
data[offset + 25] = character.Job.SecondJob;
data[offset + 26] = character.Job.ThirdJob;
data[offset + 27] = character.Job.FourthJob;
data[48 + (i * 132)] = (byte)character.Gender;
BinaryPrimitives.WriteUInt16LittleEndian(data.Slice(49 + (i * 132), 2), character.Level);
data[51 + (i * 132)] = (byte)character.Experience;
data[offset + 28] = (byte)character.Gender;
BinaryPrimitives.WriteUInt16LittleEndian(data.Slice(offset + 29, 2), character.Level);
data[offset + 31] = (byte)character.Experience;
// Stats
BinaryPrimitives.WriteInt16LittleEndian(data.Slice(52 + (i * 132), 2), character.Stats.Strength);
BinaryPrimitives.WriteInt16LittleEndian(data.Slice(54 + (i * 132), 2), character.Stats.Dexterity);
BinaryPrimitives.WriteInt16LittleEndian(data.Slice(56 + (i * 132), 2), character.Stats.Intelligence);
BinaryPrimitives.WriteInt16LittleEndian(data.Slice(58 + (i * 132), 2), character.Stats.Vitality);
BinaryPrimitives.WriteInt16LittleEndian(data.Slice(60 + (i * 132), 2), character.Stats.Luck);
BinaryPrimitives.WriteInt16LittleEndian(data.Slice(62 + (i * 132), 2), character.Stats.Wisdom);
BinaryPrimitives.WriteInt16LittleEndian(data.Slice(offset + 32, 2), character.Stats.Strength);
BinaryPrimitives.WriteInt16LittleEndian(data.Slice(offset + 34, 2), character.Stats.Dexterity);
BinaryPrimitives.WriteInt16LittleEndian(data.Slice(offset + 36, 2), character.Stats.Intelligence);
BinaryPrimitives.WriteInt16LittleEndian(data.Slice(offset + 38, 2), character.Stats.Vitality);
BinaryPrimitives.WriteInt16LittleEndian(data.Slice(offset + 40, 2), character.Stats.Luck);
BinaryPrimitives.WriteInt16LittleEndian(data.Slice(offset + 42, 2), character.Stats.Wisdom);
BinaryPrimitives.WriteInt32LittleEndian(data.Slice(64 + (i * 132), 4), character.Health);
BinaryPrimitives.WriteInt32LittleEndian(data.Slice(68 + (i * 132), 4), character.Mana);
BinaryPrimitives.WriteInt32LittleEndian(data.Slice(offset + 44, 4), character.Health);
BinaryPrimitives.WriteInt32LittleEndian(data.Slice(offset + 48, 4), character.Mana);
for (var j = 0; j < 20; j++)
{
// Equipped Items
BinaryPrimitives.WriteUInt16LittleEndian(data.Slice(72 + (i * 132) + (j * 2), 2),
BinaryPrimitives.WriteUInt16LittleEndian(data.Slice(offset + 52 + j * 2, 2),
character.EquippedItems.Length > j ? character.EquippedItems[j] : (ushort)0);
// Equipped Cash Items
BinaryPrimitives.WriteUInt16LittleEndian(data.Slice(112 + (i * 132) + (j * 2), 2),
BinaryPrimitives.WriteUInt16LittleEndian(data.Slice(offset + 92 + j * 2, 2),
character.EquippedCashItems.Length > j ? character.EquippedCashItems[j] : (ushort)0);
}
}

View file

@ -20,6 +20,8 @@ public class CharacterCreationResponsePacket : IPacket
{
Span<byte> data = stackalloc byte[1 + 132];
data[0] = isDuplicate ? (byte)1 : (byte)0;
// Character Data
BinaryPrimitives.WriteInt32LittleEndian(data.Slice(1, 4), Slot);
Encoding.ASCII.GetBytes(Character.Name, data.Slice(5, 20));