[v0.1.0] database implementation

not finished. early wip state
This commit is contained in:
BreadDEV 2024-03-06 22:00:12 +07:00
parent 8c6a533918
commit 281d2789ea
24 changed files with 637 additions and 29 deletions

View file

@ -72,4 +72,9 @@ public class LogicClientAvatar : LogicBase
checksumEncoder.WriteInt(this.TutorialFlags);
}
public void SetTutorialFlags(int tutorialFlags)
{
this.TutorialFlags |= tutorialFlags;
}
}

View file

@ -0,0 +1,46 @@
using Supercell.GUT.Logic.Message.Attributes;
using Supercell.GUT.Titan.Logic.Message;
namespace Supercell.GUT.Logic.Message.Account;
[VersionedMessage(10200)]
public class CreateAvatarMessage : VersionedMessage
{
public string? Name { get; set; }
public CreateAvatarMessage() : base(0)
{
Name = null;
}
public override void Encode()
{
base.Encode();
ByteStream.WriteString(Name);
}
public override void Decode()
{
base.Decode();
Name = ByteStream.ReadString();
}
public override int GetMessageType()
{
return 10200;
}
public override int GetServiceNodeType()
{
return 1;
}
public override void Destruct()
{
base.Destruct();
Name = null;
}
}

View file

@ -0,0 +1,33 @@
using Supercell.GUT.Logic.Message.Attributes;
using Supercell.GUT.Titan.Logic.Message;
namespace Supercell.GUT.Logic.Message.Account;
[VersionedMessage(10108)]
public class KeepAliveMessage : VersionedMessage
{
public KeepAliveMessage() : base(0)
{
;
}
public override void Encode()
{
base.Encode();
}
public override void Decode()
{
base.Decode();
}
public override int GetMessageType()
{
return 10108;
}
public override int GetServiceNodeType()
{
return 1;
}
}

View file

@ -0,0 +1,42 @@
using Supercell.GUT.Logic.Message.Attributes;
using Supercell.GUT.Titan.Logic.Message;
namespace Supercell.GUT.Logic.Message.Avatar;
[VersionedMessage(20107)]
public class AddableFriendsMessage : VersionedMessage
{
public AddableFriendsMessage() : base(0)
{
;
}
public override void Encode()
{
base.Encode();
this.ByteStream.WriteInt(-1);
}
public override void Decode()
{
base.Decode();
this.ByteStream.ReadInt();
}
public override int GetMessageType()
{
return 20107;
}
public override int GetServiceNodeType()
{
return 3;
}
public override void Destruct()
{
base.Destruct();
}
}

View file

@ -0,0 +1,62 @@
using Supercell.GUT.Logic.Message.Attributes;
using Supercell.GUT.Titan.Logic.Message;
using Supercell.GUT.Titan.Logic.Util;
namespace Supercell.GUT.Logic.Message.Avatar;
[VersionedMessage(10503)]
public class AskForAddableFriendsMessage : VersionedMessage
{
public LogicArrayList<string> FacebookIds { get; set; }
public LogicArrayList<string> GamecenterIds { get; set; }
public AskForAddableFriendsMessage() : base(0)
{
this.FacebookIds = new LogicArrayList<string>();
this.GamecenterIds = new LogicArrayList<string>();
}
public override void Destruct()
{
base.Destruct();
this.FacebookIds.Clear();
this.GamecenterIds.Clear();
}
public override void Encode()
{
base.Encode();
int size = this.FacebookIds.Size();
this.ByteStream.WriteInt(size);
for (int i = 0; i < size; i++)
{
this.ByteStream.WriteString(this.FacebookIds[i]);
}
size = this.GamecenterIds.Size();
this.ByteStream.WriteInt(size);
for (int i = 0; i < size; i++)
{
this.ByteStream.WriteString(this.GamecenterIds[i]);
}
}
public override void Decode()
{
base.Decode();
}
public override int GetMessageType()
{
return 10503;
}
public override int GetServiceNodeType()
{
return 3;
}
}

View file

@ -0,0 +1,23 @@
using Supercell.GUT.Logic.Message.Attributes;
using Supercell.GUT.Titan.Logic.Message;
namespace Supercell.GUT.Logic.Message.Avatar;
[VersionedMessage(10504)]
public class AskForFriendListMessage : VersionedMessage
{
public AskForFriendListMessage() : base(0)
{
;
}
public override int GetMessageType()
{
return 10504;
}
public override int GetServiceNodeType()
{
return 3;
}
}

View file

@ -0,0 +1,37 @@
using Supercell.GUT.Logic.Message.Attributes;
using Supercell.GUT.Titan.Logic.Message;
namespace Supercell.GUT.Logic.Message.Avatar;
[VersionedMessage(20105)]
public class FriendListMessage : VersionedMessage
{
public FriendListMessage() : base(0)
{
;
}
public override void Encode()
{
base.Encode();
this.ByteStream.WriteInt(-1);
}
public override void Decode()
{
base.Decode();
this.ByteStream.ReadInt();
}
public override int GetMessageType()
{
return 20105;
}
public override int GetServiceNodeType()
{
return 3;
}
}

View file

@ -0,0 +1,46 @@
using Supercell.GUT.Logic.Message.Attributes;
using Supercell.GUT.Titan.Logic.Message;
namespace Supercell.GUT.Logic.Message.Avatar;
[VersionedMessage(10210)]
public class TutorialProgressUpdateMessage : VersionedMessage
{
public int TutorialFlags { get; set; }
public TutorialProgressUpdateMessage() : base(0)
{
TutorialFlags = 0;
}
public override void Destruct()
{
base.Destruct();
TutorialFlags = 0;
}
public override void Encode()
{
base.Encode();
ByteStream.WriteInt(TutorialFlags);
}
public override void Decode()
{
base.Decode();
TutorialFlags = ByteStream.ReadInt();
}
public override int GetMessageType()
{
return 10210;
}
public override int GetServiceNodeType()
{
return 3;
}
}

View file

@ -0,0 +1,54 @@
using Microsoft.Extensions.Options;
using MongoDB.Driver;
using Supercell.GUT.Server.Database.Document;
using Supercell.GUT.Server.Database.Options;
namespace Supercell.GUT.Server.Database;
public class DatabaseManager
{
private const string IdentifierCounterCollection = "t_id_counter";
private static readonly FindOneAndUpdateOptions<IdentifierCounterDocument> s_counterUpdateOptions = new() { ReturnDocument = ReturnDocument.After };
private readonly DatabaseOptions _options;
private readonly MongoClient _client;
private readonly IMongoDatabase _database;
private readonly IMongoCollection<IdentifierCounterDocument> _idCounters;
public DatabaseManager(IOptions<DatabaseOptions> options)
{
_options = options.Value;
_client = new MongoClient(_options.MongoConnectionString);
_database = _client.GetDatabase(_options.DatabaseName);
_idCounters = _database.GetCollection<IdentifierCounterDocument>(IdentifierCounterCollection);
}
public IMongoCollection<TDatabaseDocument> GetCollection<TDatabaseDocument>(string collectionName) where TDatabaseDocument : IDatabaseDocument
{
return _database.GetCollection<TDatabaseDocument>(collectionName);
}
public async Task<int> GetNextLowIdAsync(string counterName)
{
var cursor = await _idCounters.FindAsync(doc => doc.CounterName == counterName);
if (!cursor.Any())
{
await _idCounters.InsertOneAsync(new IdentifierCounterDocument
{
CounterName = counterName,
CurrentLowId = 0
});
}
IdentifierCounterDocument? document = await _idCounters.FindOneAndUpdateAsync<IdentifierCounterDocument>(
x => x.CounterName == counterName,
Builders<IdentifierCounterDocument>.Update.Inc(nameof(IdentifierCounterDocument.CurrentLowId), 1),
s_counterUpdateOptions);
return document.CurrentLowId;
}
}

View file

@ -0,0 +1,19 @@
using Supercell.GUT.Titan.Logic.Math;
namespace Supercell.GUT.Server.Database.Document;
public class AccountDocument : MongoSerializeableBase, IDatabaseDocument
{
public LogicLong DocumentId { get; set; }
public string SessionKey { get; set; } = string.Empty;
public AccountDocument()
{
DocumentId = new();
}
public AccountDocument(LogicLong id, string token)
{
DocumentId = id;
SessionKey = token;
}
}

View file

@ -0,0 +1,28 @@
using Supercell.GUT.Logic.Avatar;
using Supercell.GUT.Titan.Logic.Math;
namespace Supercell.GUT.Server.Database.Document;
public class AvatarDocument : MongoSerializeableBase, IDatabaseDocument
{
public LogicLong DocumentId { get; set; }
public LogicClientAvatar LogicClientAvatar { get; set; } = new();
public int LastSaveTime { get; set; }
public AvatarDocument()
{
DocumentId = new();
}
public AvatarDocument(LogicLong id)
{
DocumentId = id;
}
public void SetId(LogicLong id)
{
LogicClientAvatar.Id = id;
}
}

View file

@ -0,0 +1,8 @@
using Supercell.GUT.Titan.Logic.Math;
namespace Supercell.GUT.Server.Database.Document;
public interface IDatabaseDocument
{
LogicLong DocumentId { get; set; }
}

View file

@ -0,0 +1,7 @@
namespace Supercell.GUT.Server.Database.Document;
internal class IdentifierCounterDocument : MongoSerializeableBase
{
public string CounterName { get; set; } = string.Empty;
public int CurrentLowId { get; set; }
}

View file

@ -0,0 +1,13 @@
using System.Text.Json.Serialization;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace Supercell.GUT.Server.Database.Document;
public abstract class MongoSerializeableBase
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
[JsonIgnore]
public string? MongoId { get; set; }
}

View file

@ -0,0 +1,7 @@
namespace Supercell.GUT.Server.Database.Options;
public class DatabaseOptions
{
public required string MongoConnectionString { get; set; }
public required string DatabaseName { get; set; }
}

View file

@ -0,0 +1,86 @@
using MongoDB.Driver;
using Supercell.GUT.Server.Database;
using Supercell.GUT.Server.Database.Document;
using Supercell.GUT.Server.Util;
using Supercell.GUT.Titan.Logic.Math;
namespace Supercell.GUT.Server.Document;
internal class DocumentManager
{
private const string AccountCollectionName = "t_account_info";
private const string AvatarCollectionName = "t_player_avatar";
private const string AccountIdCounterName = "AccountId";
private readonly DatabaseManager _databaseManager;
public LogicLong Id { get; private set; } = new();
public AccountDocument? AccountDocument { get; private set; }
public AvatarDocument? AvatarDocument { get; private set; }
public DocumentManager(DatabaseManager databaseManager)
{
_databaseManager = databaseManager;
}
public async Task SaveAsync()
{
if (AccountDocument != null)
{
IMongoCollection<AccountDocument> accountCollection = _databaseManager.GetCollection<AccountDocument>(AccountCollectionName);
await accountCollection.ReplaceOneAsync(doc => doc.DocumentId.Equals(AccountDocument.DocumentId), AccountDocument);
}
if (AvatarDocument != null)
{
AvatarDocument.LastSaveTime = TimeUtil.GetCurrentTimestamp();
IMongoCollection<AvatarDocument> avatarCollection = _databaseManager.GetCollection<AvatarDocument>(AvatarCollectionName);
await avatarCollection.ReplaceOneAsync(doc => doc.DocumentId.Equals(AvatarDocument.DocumentId), AvatarDocument);
}
}
public async Task CreateAccount()
{
if (AccountDocument != null) throw new InvalidOperationException("DocumentManager::CreateAccount: called when AccountDocument already created!");
int lowId = await _databaseManager.GetNextLowIdAsync(AccountIdCounterName);
AccountDocument = new(new(0, lowId), "ToDoRandomToken");
IMongoCollection<AccountDocument> accountCollection = _databaseManager.GetCollection<AccountDocument>(AccountCollectionName);
await accountCollection.InsertOneAsync(AccountDocument);
Id = AccountDocument.DocumentId;
}
public async Task EnsureAccountDocument()
{
if (AccountDocument != null) return;
IMongoCollection<AccountDocument> accountCollection = _databaseManager.GetCollection<AccountDocument>(AccountCollectionName);
AccountDocument = await (await accountCollection.FindAsync(document => document.DocumentId.Equals(Id))).SingleOrDefaultAsync();
}
public async Task EnsureAvatarDocument()
{
if (AvatarDocument != null) return;
IMongoCollection<AvatarDocument> avatarCollection = _databaseManager.GetCollection<AvatarDocument>(AvatarCollectionName);
AvatarDocument = await (await avatarCollection.FindAsync(document => document.DocumentId.Equals(Id))).SingleOrDefaultAsync();
if (AvatarDocument == null)
{
AvatarDocument = new(Id);
await avatarCollection.InsertOneAsync(AvatarDocument);
}
}
public void SetDocumentId(LogicLong id)
{
if (Id.LowerInt > 0) throw new InvalidOperationException("DocumentManager::SetDocumentId: trying to override Id.");
Id = id;
}
}

View file

@ -1,4 +1,5 @@
using Supercell.GUT.Logic;
using Supercell.GUT.Logic.Avatar;
using Supercell.GUT.Server.Protocol;
using Supercell.GUT.Titan.Logic.Message;
@ -11,6 +12,8 @@ internal class ClientConnection
private readonly IConnectionListener _listener;
private readonly MessageManager _messageManager;
public LogicClientAvatar? LogicClientAvatar { get; set; }
private readonly byte[] _receiveBuffer;
private IProtocolEntity? _protocolEntity;
private DateTime _lastKeepAliveTime;

View file

@ -2,7 +2,10 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Supercell.GUT.Logic.Message;
using Supercell.GUT.Server.Database;
using Supercell.GUT.Server.Database.Options;
using Supercell.GUT.Server.Debugging;
using Supercell.GUT.Server.Document;
using Supercell.GUT.Server.Network;
using Supercell.GUT.Server.Network.Connection;
using Supercell.GUT.Server.Network.Options;
@ -17,6 +20,7 @@ namespace Supercell.GUT.Server;
internal static class Program
{
private const string GatewayOptionsSection = "Gateway";
private const string DatabaseOptionsSection = "Database";
private static async Task Main(string[] args)
{
@ -25,6 +29,7 @@ internal static class Program
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.Configure<GatewayOptions>(builder.Configuration.GetRequiredSection(GatewayOptionsSection));
builder.Services.Configure<DatabaseOptions>(builder.Configuration.GetRequiredSection(DatabaseOptionsSection));
builder.Services.AddHandlers();
builder.Services.AddSingleton<IDebuggerListener, ServerDebuggerListener>();
@ -34,6 +39,8 @@ internal static class Program
builder.Services.AddScoped<IConnectionListener, Messaging>();
builder.Services.AddSingleton<LogicMessageFactory, GUTMessageFactory>();
builder.Services.AddScoped<MessageManager>();
builder.Services.AddScoped<DocumentManager>();
builder.Services.AddSingleton<DatabaseManager>();
builder.Services.AddHostedService<GUTServer>();

View file

@ -1,5 +1,7 @@
using Microsoft.Extensions.Logging;
using Supercell.GUT.Logic.Message.Account;
using Supercell.GUT.Server.Database.Document;
using Supercell.GUT.Server.Document;
using Supercell.GUT.Server.Network.Connection;
using Supercell.GUT.Server.Protocol.Attributes;
@ -10,11 +12,19 @@ internal class AccountMessageHandler : MessageHandlerBase
{
private readonly ILogger _logger;
private readonly ClientConnection _connection;
private readonly DocumentManager _documentManager;
public AccountMessageHandler(ClientConnection connection, ILogger<AccountMessageHandler> logger)
public AccountMessageHandler(ClientConnection connection, ILogger<AccountMessageHandler> logger, DocumentManager documentManager)
{
_logger = logger;
_connection = connection;
_documentManager = documentManager;
}
[MessageHandler(10108)]
public async Task OnKeepAlive(KeepAliveMessage keepAliveMessage)
{
await Task.CompletedTask;
}
[MessageHandler(10105)]
@ -33,32 +43,59 @@ internal class AccountMessageHandler : MessageHandlerBase
[MessageHandler(10103)]
public async Task OnCreateAccount(CreateAccountMessage createAccountMessage)
{
_logger.LogInformation("Creating account! FacebookId: {fid} | GameCenterId: {gcid}",
createAccountMessage.FacebookId,
createAccountMessage.GameCenterId);
await _documentManager.CreateAccount();
AccountDocument? accountDocument = _documentManager.AccountDocument;
if (accountDocument == null)
{
await _connection.SendMessage(new CreateAccountFailedMessage()
{
ErrorCode = 1
});
return;
}
await _connection.SendMessage(new CreateAccountOkMessage()
{
AccountIdHigherInt = 0,
AccountIdLowerInt = 1,
SessionKey = "telegram_is_@BL4D3_BR34D"
AccountIdHigherInt = accountDocument.DocumentId.HigherInt,
AccountIdLowerInt = accountDocument.DocumentId.LowerInt,
SessionKey = accountDocument.SessionKey
});
}
[MessageHandler(10102)]
public async Task OnLoginUsingSession(LoginUsingSessionMessage loginUsingSessionMessage)
{
_logger.LogInformation("Logging account! sessionkey: {sk} | GameCenterId: {gcid} | GameVersion: {cgv} | account: ({h}-{l})",
loginUsingSessionMessage.SessionKey,
loginUsingSessionMessage.GamecenterId,
loginUsingSessionMessage.ClientGameVersion,
loginUsingSessionMessage.AccountIdHigherInt,
loginUsingSessionMessage.AccountIdLowerInt);
if (_documentManager.Id.LowerInt == 0)
_documentManager.SetDocumentId(new (loginUsingSessionMessage.AccountIdHigherInt, loginUsingSessionMessage.AccountIdLowerInt));
await _documentManager.EnsureAccountDocument();
AccountDocument? accountDocument = _documentManager.AccountDocument;
if (accountDocument == null)
{
await _connection.SendMessage(new LoginFailedMessage()
{
ErrorCode = 2
});
return;
}
else if (accountDocument.SessionKey != loginUsingSessionMessage.SessionKey)
{
await _connection.SendMessage(new LoginFailedMessage()
{
ErrorCode = 5
});
return;
}
await _connection.SendMessage(new LoginOkMessage()
{
PrimaryAvatarIdHigherInt = loginUsingSessionMessage.AccountIdHigherInt,
PrimaryAvatarIdLowerInt = loginUsingSessionMessage.AccountIdLowerInt,
PrimaryAvatarIdHigherInt = accountDocument.DocumentId.HigherInt,
PrimaryAvatarIdLowerInt = accountDocument.DocumentId.LowerInt,
Fingerprint = "fece25cfc941db1a5bb3e79d4e6a60c34659f49e"
});

View file

@ -1,9 +1,9 @@
using Microsoft.Extensions.Logging;
using Supercell.GUT.Logic.Avatar;
using Supercell.GUT.Logic.Message.Avatar;
using Supercell.GUT.Server.Document;
using Supercell.GUT.Server.Network.Connection;
using Supercell.GUT.Server.Protocol.Attributes;
using Supercell.GUT.Titan.Logic.Math;
namespace Supercell.GUT.Server.Protocol.Handlers;
@ -12,31 +12,52 @@ internal class AvatarMessageHandler : MessageHandlerBase
{
private readonly ILogger _logger;
private readonly ClientConnection _connection;
private readonly DocumentManager _documentManager;
public AvatarMessageHandler(ClientConnection connection, ILogger<AvatarMessageHandler> logger)
public AvatarMessageHandler(ClientConnection connection, ILogger<AvatarMessageHandler> logger, DocumentManager documentManager)
{
_logger = logger;
_connection = connection;
_documentManager = documentManager;
}
[MessageHandler(10201)]
public async Task OnSelectAvatar(SelectAvatarMessage selectAvatarMessage)
{
_logger.LogInformation("selecting avatar! avatar: ({h}-{l})",
selectAvatarMessage.AvatarIdHigherInt,
selectAvatarMessage.AvatarIdLowerInt);
await _documentManager.EnsureAvatarDocument();
LogicClientAvatar logicClientAvatar = _documentManager.AvatarDocument!.LogicClientAvatar;
if (logicClientAvatar.Id.LowerInt == 0)
{
_documentManager.AvatarDocument.SetId(_documentManager.AvatarDocument.DocumentId);
await _documentManager.SaveAsync();
}
_connection.LogicClientAvatar = logicClientAvatar;
await _connection.SendMessage(new AvatarDataMessage()
{
LogicClientAvatar = new LogicClientAvatar()
{
Id = new LogicLong(selectAvatarMessage.AvatarIdHigherInt, selectAvatarMessage.AvatarIdLowerInt),
Name = "BreadDEV",
AvatarCode = "a1",
TutorialFlags = 999999
}
LogicClientAvatar = _connection.LogicClientAvatar
});
}
[MessageHandler(10210)]
public async Task OnTutorialProgressUpdate(TutorialProgressUpdateMessage tutorialProgressUpdateMessage)
{
_connection.LogicClientAvatar!.SetTutorialFlags(tutorialProgressUpdateMessage.TutorialFlags);
await _documentManager.SaveAsync();
}
[MessageHandler(10504)]
public async Task OnAskForFriendList(AskForFriendListMessage askForFriendListMessage)
{
await _connection.SendMessage(new FriendListMessage());
}
[MessageHandler(10503)]
public async Task OnAskForAddableFriends(AskForAddableFriendsMessage askForAddableFriendsMessage)
{
await _connection.SendMessage(new AddableFriendsMessage());
}
}

View file

@ -9,6 +9,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="MongoDB.Driver" Version="2.24.0" />
</ItemGroup>
<ItemGroup>

View file

@ -0,0 +1,11 @@
namespace Supercell.GUT.Server.Util;
public static class TimeUtil
{
private static readonly DateTime s_unixTime = new(1970, 1, 1);
public static int GetCurrentTimestamp()
{
return (int)DateTime.UtcNow.Subtract(s_unixTime).TotalSeconds;
}
}

View file

@ -2,5 +2,9 @@
"Gateway": {
"Host": "0.0.0.0",
"Port": 9339
},
"Database": {
"MongoConnectionString": "mongodb://127.0.0.1:27017",
"DatabaseName": "gut"
}
}

View file

@ -36,4 +36,12 @@ public class LogicLong
this.HigherInt = byteStream.ReadInt();
this.LowerInt = byteStream.ReadInt();
}
public override bool Equals(object? obj)
{
if (obj is LogicLong logicLong)
return logicLong.HigherInt == this.HigherInt && logicLong.LowerInt == this.LowerInt;
return false;
}
}