forked from CodeSpartan/MMOKitPersistenceServer
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathGameLogic.cs
More file actions
399 lines (356 loc) · 16.6 KB
/
Copy pathGameLogic.cs
File metadata and controls
399 lines (356 loc) · 16.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
using Microsoft.AspNetCore.Hosting.Server;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Xml.Linq;
namespace PersistenceServer
{
public class GameLogic
{
// When the player first logs in, he gets a connection cookie, and we save him as <cookie, accountId>
// He then selects a character to log in with, and then loads a new level, which causes a disconnect
// Once he reconnects, he will send his desired character id and a cookie to the game server
// The game server will pass it along to the persistence server
// And we'll check if character id exists on the accountId - if it does, we'll send the character to the game server
// And the game server will spawn the character for the player
// And when the player reconnects to the persistence server, he will send exactly the same thing - cookie and character id
// And we'll log him in, if everything checks out
// Hence, _connectionCookies survives disconnects
readonly Dictionary<string, int> _connectionCookies; // <cookie, account id>
readonly Dictionary<UserConnection, GameServer> _gameServers;
readonly Dictionary<string, List<GameServer>> _gameServersByZone; // all instances hosting a particular zone
readonly Dictionary<string, GameServer> _gameServersByGuid;
readonly Dictionary<string, Party> _partiesByGuid;
Dictionary<int, Guild> _guildsById; // all guilds, even if players of said guilds didn't come online this session
readonly Dictionary<int, string> _playersLastServerId; // when a server sends a GetCharacter RPC, we record this server as last known "owner" of a player
Dictionary<int, string> _storedPlayerPartyId; // when a player disconnects, we store his previously known party id so he can be reconnected with the party when he returns
// Dictionaries below do not survive reconnects, they have to be repopulated on reconnects
readonly Dictionary<UserConnection, int> _accountIdByConnection;
readonly Dictionary<int, UserConnection> _connectionByAccountId;
readonly Dictionary<UserConnection, int> _charIdByConnection;
readonly Dictionary<int, UserConnection> _connectionByCharId;
readonly Dictionary<string, Player> _playersByName;
readonly Dictionary<int, Player> _playersById;
readonly Dictionary<UserConnection, Player> _playersByConnection;
public GameLogic()
{
_connectionCookies = new();
_accountIdByConnection = new();
_connectionByAccountId = new();
_charIdByConnection = new();
_connectionByCharId = new();
_playersByName = new();
_playersById = new();
_playersByConnection = new();
_gameServers = new();
_gameServersByZone = new();
_gameServersByGuid = new();
_guildsById = new();
_partiesByGuid = new();
_playersLastServerId = new();
_storedPlayerPartyId = new();
}
public int GetAccountId(UserConnection conn)
{
if(_accountIdByConnection.TryGetValue(conn, out var id))
return id;
return -1;
}
// Called when the user logs in through the initial menu - he's not in the game world and has no character yet
public void UserLoggedIn(int accountId, string cookie, UserConnection conn)
{
conn.Cookie = cookie;
_connectionCookies.Add(cookie, accountId);
if (_accountIdByConnection.Remove(conn)) // remove if key exists
{
Console.WriteLine("User tried to log in twice, we must never reach here");
// try to recover
}
_accountIdByConnection.Add(conn, accountId);
// if release, don't allow multiple connections from one account
// but in debug, it's not unexpected, because we may open two windows in PIE and get two characters from the same account
// @TODO: maybe we should make a bool allowMultipleCharacters and change ConnectionByAccountId to Dictionary<int, List<UserConnection>>
#if RELEASE
if (_connectionByAccountId.TryGetValue(accountId, out var oldConn))
{
InvalidateCookieForConnection(oldConn);
DisconnectPlayerFromAllGameServers(oldConn);
_ = oldConn.Disconnect(); // not awaited
UserDisconnected(oldConn); // fire this immediately, because otherwise it would fire too late due to threads jumping
}
#else
if (!_connectionByAccountId.ContainsKey(accountId))
_connectionByAccountId.Add(accountId, conn);
#endif
}
public int GetAccountIdByCookie(string cookie)
{
return _connectionCookies.TryGetValue(cookie, out var connectionValue) ? connectionValue : -1;
}
// IMPORTANT: don't call it for logged-in characters, because two logged in characters from the same account in PIE will get the same connection,
// which will likely cause bugs. Unless you explicitely forbid the functionality associated with it in PIE.
public UserConnection? GetConnectionByAccountId(int accountId)
{
return _connectionByAccountId.TryGetValue(accountId, out var connection) ? connection : null;
}
public void UserDisconnected(UserConnection conn)
{
if (_gameServers.TryGetValue(conn, out GameServer? gameServer))
{
if (_gameServersByZone.TryGetValue(gameServer.Zone, out List<GameServer>? zoneInstances))
{
zoneInstances.Remove(gameServer);
}
_gameServersByGuid.Remove(conn.Id.ToString());
_gameServers.Remove(conn);
Console.WriteLine($"{DateTime.Now:HH:mm} Game server disconnected");
}
if (_charIdByConnection.TryGetValue(conn, out var playerId))
{
_connectionByCharId.Remove(playerId);
_charIdByConnection.Remove(conn);
}
if (_accountIdByConnection.TryGetValue(conn, out var userid))
{
_connectionByAccountId.Remove(userid);
_accountIdByConnection.Remove(conn);
}
if (_playersByConnection.TryGetValue(conn, out var player))
{
var guildId = player.GuildId;
if (guildId != -1 && _guildsById.TryGetValue(guildId, out var guild)) {
guild.OnPlayerDisconnected(player);
}
var charname = player.Name;
var charId = player.CharId;
Console.WriteLine($"{DateTime.Now:HH:mm} Player disconnected: {charname}");
_playersByConnection.Remove(conn);
_playersByName.Remove(charname);
_playersById.Remove(charId);
}
}
// Called when the user connects from the game world - he now has a character
public Player UserReconnected(UserConnection newConn, DatabaseCharacterInfo charInfo)
{
if (_connectionByAccountId.TryGetValue(charInfo.AccountId, out var oldConn))
{
// If RELEASE, we don't allow multiple characters from one account
// But in debug, it's not unexpected, because we may open two windows in PIE and get two characters from the same account
// @TODO: maybe we should make a bool allowMultipleCharacters and change ConnectionByAccountId to Dictionary<int, List<UserConnection>>
#if RELEASE
InvalidateCookieForConnection(oldConn);
DisconnectPlayerFromAllGameServers(oldConn);
_ = oldConn.Disconnect(); // not awaited
UserDisconnected(oldConn); // fire this immediately, because otherwise it would fire too late due to threads jumping
_accountIdByConnection.Add(newConn, charInfo.AccountId);
_connectionByAccountId.Add(charInfo.AccountId, newConn);
#endif
}
else {
_accountIdByConnection.Add(newConn, charInfo.AccountId);
_connectionByAccountId.Add(charInfo.AccountId, newConn);
}
_connectionByCharId.Add(charInfo.CharId, newConn);
_charIdByConnection.Add(newConn, charInfo.CharId);
Player newPlayer = new(newConn, charInfo);
_playersByConnection.Add(newConn, newPlayer);
_playersByName.Add(charInfo.Name, newPlayer);
_playersById.Add(charInfo.CharId, newPlayer);
if (charInfo.Guild != null)
{
if (_guildsById.TryGetValue((int)charInfo.Guild, out var guild))
guild.OnPlayerConnected(newPlayer);
else
Console.WriteLine("Player connected with a guild id that doesn't exist. This should never happen, look into it.");
}
return newPlayer;
}
public void ServerConnected(UserConnection conn, GameServer server)
{
_gameServers.Add(conn, server);
_gameServersByGuid.Add(conn.Id.ToString(), server);
if (_gameServersByZone.TryGetValue(server.Zone, out List<GameServer>? serverInstancesForZone))
{
serverInstancesForZone.Add(server);
} else
{
_gameServersByZone.Add(server.Zone, new List<GameServer> { server });
}
}
public bool IsServer(UserConnection conn)
{
return _gameServers.ContainsKey(conn);
}
public async Task<GameServer> GetOrStartServerForZone(string zone)
{
//@TODO: launch an instance if there isn't a server running a particular zone, or if we're above player limit in all instances
// but for now, let's just return the first instance
if (_gameServersByZone.TryGetValue(zone, out List<GameServer>? serverInstances))
{
//@TODO: check if there's an instance with sufficient free player slots, if not launch one, etc
return serverInstances[0];
}
else
{
// temporarily awaiting Task.CompletedTask to avoid the compiler warning an in incomplete method
await Task.CompletedTask;
throw new NotImplementedException("This method is not implemented yet.");
}
}
public UserConnection[] GetAllPlayerConnections()
{
return _playersByConnection.Keys.ToArray();
}
public Player? GetPlayerByName(string name)
{
return _playersByName.TryGetValue(name, out var player) ? player : null;
}
public Player? GetPlayerById(int charId)
{
return _playersById.TryGetValue(charId, out var player) ? player : null;
}
public Player? GetPlayerByConnection(UserConnection conn)
{
return _playersByConnection.TryGetValue(conn, out var player) ? player : null;
}
public UserConnection? GetConnectionByCharId(int charId)
{
return _connectionByCharId.TryGetValue(charId, out var conn) ? conn : null;
}
public int GetPlayersOnline()
{
return _playersByConnection.Count;
}
public GameServer? GetServerByConnection(UserConnection conn)
{
return _gameServers.TryGetValue(conn, out var server) ? server : null;
}
public GameServer? GetServerByGuid(string guid)
{
return _gameServersByGuid.TryGetValue(guid, out var server) ? server : null;
}
public string GetPlayerName(UserConnection conn)
{
return _playersByConnection.TryGetValue(conn, out var player) ? player.Name : "";
}
public UserConnection[] GetAllServerConnections()
{
return _gameServers.Keys.ToArray();
}
public void DisconnectPlayerFromAllGameServers(UserConnection conn)
{
if (_charIdByConnection.TryGetValue(conn, out var oldCharId))
{
byte[] msgToServers = BaseRpc.MergeByteArrays(BaseRpc.ToBytes(RpcType.RpcForceDisconnectPlayer), BaseRpc.ToBytes(oldCharId));
foreach (var serverConn in GetAllServerConnections())
{
serverConn.Send(msgToServers);
}
}
}
public void InvalidateCookieForConnection(UserConnection conn)
{
_connectionCookies.Remove(conn.Cookie);
}
public Guild? GetPlayerGuild(UserConnection conn)
{
if (_playersByConnection.TryGetValue(conn, out var player))
{
var guildId = player.GuildId;
return _guildsById.TryGetValue(guildId, out var guild) ? guild : null;
}
return null;
}
public Guild? GetGuildById(int guildId)
{
return _guildsById.TryGetValue(guildId, out var guild) ? guild : null;
}
public void AssignGuilds(Dictionary<int, Guild> guilds)
{
_guildsById = guilds;
}
public void CreateGuild(Guild createdGuild, Player guildLeader)
{
_guildsById.Add(createdGuild.Id, createdGuild);
createdGuild.PopulateMember(guildLeader.CharId, guildLeader.Name, 0);
createdGuild.OnPlayerConnected(guildLeader);
guildLeader.GuildId = createdGuild.Id;
guildLeader.GuildRank = 0; // 0 is leader
}
public void DeleteGuild(int guildId)
{
_guildsById.Remove(guildId);
}
public void SetPlayersServer(int charId, string serverId)
{
if (_playersLastServerId.ContainsKey(charId))
{
_playersLastServerId[charId] = serverId;
} else
{
_playersLastServerId.Add(charId, serverId);
}
}
public GameServer? GetPlayerServer(int charId)
{
if (_playersLastServerId.TryGetValue(charId, out var serverGuid))
{
return GetServerByGuid(serverGuid);
}
return null;
}
public void AddParty(Party party)
{
_partiesByGuid.Add(party.Id, party);
}
public void RemoveParty(Party party)
{
_partiesByGuid.Remove(party.Id);
}
public Party? GetPartyById(string Id)
{
return _partiesByGuid.TryGetValue(Id, out var party) ? party : null;
}
// store party id for a disconnected character
// if the character reconnects before getting kicked, we'll be able to reassign him his party ref
public void StoreDisconnectedPlayerPartyId(int charId, string partyId)
{
_storedPlayerPartyId[charId] = partyId;
}
public void UnstoreDisconnectedPlayerPartyId(int charId)
{
_storedPlayerPartyId.Remove(charId);
}
public string? GetStoredDisconnectedPlayerPartyId(int charId)
{
return _storedPlayerPartyId.TryGetValue(charId, out var partyId) ? partyId : null;
}
#pragma warning disable CA1822 // remove the "make it static" warning, AddGuildMember may need to operate on fields later on
public void AddGuildMember(Guild guild, Player player, int rank)
#pragma warning restore CA1822
{
guild.PopulateMember(player.CharId, player.Name, rank);
guild.OnPlayerConnected(player);
player.GuildId = guild.Id;
player.GuildRank = rank;
}
#pragma warning disable CA1822 // remove the "make it static" warning, DeleteGuildMember may need to operate on fields later on
// deletion assumes the player isn't online, so we only need to remove from Members
public void DeleteGuildMember(Guild guild, int charId)
#pragma warning restore CA1822
{
guild.RemoveMemberById(charId);
}
#pragma warning disable CA1822 // remove the "make it static" warning, RemoveGuildMember may need to operate on fields later on
public void RemoveGuildMember(Guild guild, int charId, Player? player)
#pragma warning restore CA1822
{
if (player != null)
{
guild.OnPlayerDisconnected(player);
player.GuildId = -1;
player.GuildRank = -1;
}
guild.RemoveMemberById(charId);
}
}
}