/
matchmakingservice.lua
2448 lines (2117 loc) · 82.5 KB
/
matchmakingservice.lua
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
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
local CLOSED = false
local PLAYERSADDED = {}
local PLAYERSREMOVED = {}
local PLAYERSADDEDTHISWAVE = {}
local MemoryStoreService = game:GetService("MemoryStoreService")
local TeleportService = game:GetService("TeleportService")
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local MessagingService = game:GetService("MessagingService")
local DatastoreService = game:GetService("DataStoreService")
local SkillDatastore = DatastoreService:GetDataStore("MATCHMAKINGSERVICE_SKILLS")
-- local Cache = require(script.Cache)
local OpenSkill = require(script.OpenSkill)
local Signal = require(script.Signal)
local MainMemory = MemoryStoreService:GetSortedMap("MATCHMAKINGSERVICE")
local JoinableGamesMemory = MemoryStoreService:GetSortedMap("MATCHMAKINGSERVICE_JOINABLE_GAMES")
local NonJoinableGamesMemory = MemoryStoreService:GetSortedMap("MATCHMAKINGSERVICE_NONJOINABLE_GAMES")
local MemoryQueue = MemoryStoreService:GetSortedMap("MATCHMAKINGSERVICE_QUEUE")
local TeleportDataMemory = MemoryStoreService:GetSortedMap("MATCHMAKINGSERVICE_TELEPORT_DATA")
-- GAME SERVER VARIABLES
local _gameData = nil
local _gameId = nil
local MatchmakingService = {
Singleton = nil;
Version = "3.0.0-beta.3";
Versions = {
["v1"] = 8470858629;
["v2"] = 8898097654;
["v3"] = 13533090734;
};
}
MatchmakingService.__index = MatchmakingService
-- Useful table utilities
function first(haystack, num, skip)
if haystack == nil then return nil end
if skip == nil then skip = 1 end
local toReturn = {}
for i = skip, num + (skip - 1) do
if i > #haystack then break end
table.insert(toReturn, haystack[i])
end
return toReturn
end
function find(haystack, needle)
if haystack == nil then return nil end
for i, v in ipairs(haystack) do
if needle(v) then
return i
end
end
return nil
end
function dictlen(dict)
local counter = 0
for _ in pairs(dict) do
counter += 1
end
return counter
end
function any(haystack, cond)
if haystack == nil or cond == nil then return false end
for i, v in pairs(haystack) do
if cond(v) then
return true
end
end
return false
end
function all(haystack, needles)
if haystack == nil or needles == nil then return false end
local hasNeedles = {}
for i, v in ipairs(needles) do
hasNeedles[v] = false
end
for i, v in ipairs(haystack) do
if table.find(needles, v) ~= nil then
hasNeedles[v] = true
end
end
return not any(hasNeedles, function(x) return not x end)
end
function reduce(haystack, transform, default)
local cur = default or 0
for k, v in pairs(haystack) do
cur = transform(cur, v, k, haystack)
end
return cur
end
function tableSelect(haystack, prop)
if haystack == nil then return nil end
local toReturn = {}
for i, v in ipairs(haystack) do
table.insert(toReturn, v[prop])
end
return toReturn
end
function append(tbl, tbl2)
if tbl == nil or tbl2 == nil then
return nil
end
for _, v in ipairs(tbl2) do
table.insert(tbl, v)
end
end
-- End table utilities
-- Useful utilities
-- Rounds a value to the nearest 10
function roundSkill(skill)
return math.round(skill/10) * 10
end
function checkForParties(values)
for i, v in ipairs(values) do
if v[3] ~= nil and i + v[3] > #values then
for j = #values, i, -1 do
table.remove(values, j)
end
return false
end
end
return true
end
function getFromMemory(m, k, retries)
if retries == nil then retries = 3 end
local success, response
local count = 0
while not success and count < retries do
success, response = pcall(m.GetAsync, m, k)
count += 1
if not success then task.wait(3) end
end
if not success then warn(response) end
return response
end
function updateQueue(map: string, ratingType, stringRoundedRating, role)
local now = DateTime.now().UnixTimestampMillis
local success, errorMessage
if role == nil then
role = "MMS_NO_ROLE"
end
success, errorMessage = pcall(function()
MemoryQueue:UpdateAsync("QueuedMaps", function(old)
if old == nil then
old = {map}
else
if find(old, function(v)
return v == map
end) then
return old
end
table.insert(old, map)
end
return old
end, 86400)
end)
if not success then
print("Unable to update queued maps:")
warn(errorMessage)
end
success, errorMessage = pcall(function()
MemoryQueue:UpdateAsync(map.."__QueuedRatingTypes", function(old)
if old == nil then
old = {}
old[ratingType] = {{stringRoundedRating, now}}
elseif old[ratingType] == nil then
old[ratingType] = {{stringRoundedRating, now}}
else
if find(old[ratingType], function(v)
return v[1] == stringRoundedRating
end) then
return old
end
table.insert(old[ratingType], {stringRoundedRating, now})
end
return old
end, 86400)
end)
if not success then
print("Unable to update queued rating types:")
warn(errorMessage)
end
end
-- End useful utilities
-- Private connections
-- End private connections
--- Gets or creates the top level singleton of the matchmaking service.
-- @param options - The options to provide matchmaking service.
-- @param options.MajorVersion - The major version to obtain.
-- @param options.DisableRatingSystem - Whether or not to disable the rating system.
-- @param options.DisableExpansions - Whether or not to disable queue expansions.
-- @param options.DisableGlobalEvents - Whether or not to disable global events.
-- @return MatchmakingService - The matchmaking service singleton.
type Singleton = {
AddGamePlace: (self: {}, name: string, placeId: number) -> ();
SetMatchmakingInterval: (self: {}, newInterval: number) -> ();
SetPlayerRange: (self: {}, map: string, newPlayerRange: NumberRange) -> ();
SetIsGameServer: (self: {}, newValue: boolean, updateJoinableOnLeave: boolean) -> ();
SetStartingMean: (self: {}, newStartingMean: number) -> ();
SetStartingStandardDeviation: (self: {}, newStartingStandardDeviation: number) -> ();
SetMaxPartySkillGap: (self: {}, newMaxGap: number) -> ();
SetSecondsBetweenExpansion: (self: {}, newValue: number) -> ();
SetFoundGameDelay: (self: {}, newValue: number) -> ();
Clear: (self: {}) -> ();
GetCurrentGameCode: (self: {}) -> (number?);
GetGameData: (self: {}, code: string) -> ({any?}?);
--GetUserDataId: (self: {}, player: number) -> ({any?}?);
GetUserData: (self: {}, player: Player) -> ({any?}?);
ToRatingNumber: (self:{}, openSkillObject: any) -> (number);
--GetPlayerRatingId: (self: {}, player: number, ratingType: string) -> ({mu: number, sigma: number}?);
GetPlayerRating: (self: {}, player: Player, ratingType: string) -> ({mu: number, sigma: number}?);
--SetPlayerRatingId: (self: {}, player: number, ratingType: string, rating: {mu: number, sigma: number}) -> ();
SetPlayerRating: (self: {}, player: Player, ratingType: string, rating: {mu: number, sigma: number}) -> ();
--ClearPlayerInfoId: (self: {}, playerId: number) -> ();
ClearPlayerInfo: (self: {}, player: Player) -> ();
--SetPlayerInfoId: (self: {}, player: number, code: string, ratingType: string, party: {number}, map: string, teleportAfter: number) -> ();
SetPlayerInfo: (self: {}, player: Player, code: string, ratingType: string, party: {number}, map: string, teleportAfter: number) -> ();
--GetPlayerInfoId: (self: {}, player: number) -> (PlayerInfo);
GetPlayerInfo: (self: {}, player: Player) -> (PlayerInfo);
GetQueueCounts: (self:{}) ->({any}, number);
GetAllRunningGames: (self:{}) ->({any});
GetRunningGame: (self: {}, code: string) ->({any}?);
GetQueue: (self:{}, map: string) -> ({[string]: {[any]: {any}}}?);
--QueuePlayerId: (self: {}, player: number, ratingType: string, map: string) -> (boolean);
QueuePlayer: (self: {}, player: Player, ratingType: string, map: string) -> (boolean);
--QueuePartyId: (self: {}, players: {number}, ratingType: string, map: string) -> (boolean);
QueueParty: (self: {}, players: {Player}, ratingType: string, map: string) -> (boolean);
--GetPlayerPartyId: (self: {}, player: number) -> ({number}?);
GetPlayerParty: (self: {}, player: Player) -> ({number}?);
--RemovePlayerFromQueueId: (self: {}, player: number) -> (boolean?);
RemovePlayerFromQueue: (self: {}, player: Player) -> (boolean?);
--RemovePlayersFromQueueId: (self: {}, players: {number}) -> (boolean?);
RemovePlayersFromQueue: (self: {}, players: {Player}) -> (boolean?);
--AddPlayerToGameId: (self: {}, player: number, gameId: string, updateJoinable: boolean) -> (boolean?);
AddPlayerToGame: (self: {}, player: Player, gameId: string, updateJoinable: boolean) -> (boolean?);
--AddPlayersToGameId: (self: {}, players: {number}, gameId: string, updateJoinable: boolean) -> (boolean?);
AddPlayersToGame: (self: {}, players: {Player}, gameId: string, updateJoinable: boolean) -> (boolean?);
--RemovePlayerFromGameId: (self: {}, player: number, gameId: string, updateJoinable: boolean) -> (boolean?);
RemovePlayerFromGame: (self: {}, player: Player, gameId: string, updateJoinable: boolean) -> (boolean?);
--RemovePlayersFromGameId: (self: {}, players: {number}, gameId: string, updateJoinable: boolean) -> (boolean?);
RemovePlayersFromGame: (self: {}, players: {Player}, gameId: string, updateJoinable: boolean) -> (boolean?);
--UpdateRatingsId: (self: {}, ratingType: string, ranks: {number}, teams: {{number}}) -> (boolean?);
UpdateRatings: (self: {}, ratingType: string, ranks: {number}, teams: {{Player}}) -> (boolean?);
SetJoinable: (self: {}, gameId: string, joinable: boolean) -> (boolean?);
RemoveGame: (self: {}) -> (boolean?);
StartGame: (self: {}, gameId: string, joinable: boolean) -> (boolean?);
}
function MatchmakingService.GetSingleton(options: { MajorVersion: string | nil; DisableRatingSystem: boolean | nil; DisableExpansions: boolean | nil; DisableGlobalEvents: boolean | nil;} | nil): Singleton
if options and options.MajorVersion ~= nil then
local versionToGet = options.MajorVersion
options.MajorVersion = nil
local id = MatchmakingService.Versions[versionToGet]
if id == nil then
warn("Major version " .. tostring(options.MajorVersion) .. " not found.")
end
MatchmakingService.Singleton = require(id).GetSingleton(options)
return MatchmakingService.Singleton
end
print("Retrieving MatchmakingService ("..MatchmakingService.Version..") Singleton.")
if MatchmakingService.Singleton == nil then
Players.PlayerRemoving:Connect(function(player)
if MatchmakingService.Singleton.IsGameServer then
MatchmakingService.Singleton:RemovePlayerFromGameId(player.UserId, MatchmakingService.Singleton:GetCurrentGameCode(), MatchmakingService.Singleton.UpdateJoinableOnLeave)
end
MatchmakingService.Singleton:RemovePlayerFromQueueId(player.UserId)
end)
MatchmakingService.Singleton = MatchmakingService.new(options)
task.spawn(function()
local mainJobId = getFromMemory(MainMemory, "MainJobId", 3)
local now = DateTime.now().UnixTimestampMillis
local isMain = mainJobId == nil or mainJobId[2] + 25000 <= now
if isMain and not CLOSED then
MainMemory:UpdateAsync("MainJobId", function(old)
if old == nil or old[1] == mainJobId then
return {game.JobId, now}
end
return nil
end, 86400)
end
if options == nil or not options.DisableGlobalEvents then
MessagingService:SubscribeAsync("MatchmakingServicePlayersAddedToQueue", function(message)
for _, v in ipairs(message["Data"]) do
if Players:GetPlayerByUserId(v[1]) ~= nil then continue end
MatchmakingService.Singleton.PlayerAddedToQueue:Fire(v[1], v[2], v[3], v[4])
end
end)
MessagingService:SubscribeAsync("MatchmakingServicePlayersRemovedFromQueue", function(message)
for _, v in ipairs(message["Data"]) do
if Players:GetPlayerByUserId(v[1]) ~= nil then continue end
MatchmakingService.Singleton.PlayerRemovedFromQueue:Fire(v[1], v[2], v[3], v[4])
end
end)
while not CLOSED do
task.wait(5)
if #PLAYERSADDED > 0 then
MessagingService:PublishAsync("MatchmakingServicePlayersAddedToQueue", PLAYERSADDED)
table.clear(PLAYERSADDED)
end
if #PLAYERSREMOVED > 0 then
MessagingService:PublishAsync("MatchmakingServicePlayersRemovedFromQueue", PLAYERSREMOVED)
table.clear(PLAYERSREMOVED)
end
table.clear(PLAYERSADDEDTHISWAVE)
end
end
end)
end
return MatchmakingService.Singleton
end
--- Sets the matchmaking interval.
-- @param newInterval The new matchmaking interval.
function MatchmakingService:SetMatchmakingInterval(newInterval: number)
self.MatchmakingInterval = newInterval
end
--- Sets the min/max players.
-- @param map The map the player range applies to.
-- @param newPlayerRange The NumberRange with the min and max players.
function MatchmakingService:SetPlayerRange(map: string, newPlayerRange: NumberRange)
if newPlayerRange.Max > 100 then
warn("Maximum players has a cap of 100.")
end
self.PlayerRanges[map] = {
["MMS_NO_ROLE"] = {
["Min"] = newPlayerRange.Min,
["Max"] = newPlayerRange.Max
}
}
end
type RoleRange = {
Min: number,
Max: number
}
type MapRoles = { [string]: RoleRange }
--- Sets the min/max players.
-- @param map The map the player range applies to.
-- @param newMapRoles The MapRoles to apply
function MatchmakingService:SetMapRoles(map: string, newMapRoles: MapRoles)
for _, role in newMapRoles do
if role.Max > 100 then
warn("Maximum players has a cap of 100.")
end
end
self.PlayerRanges[map] = newMapRoles
end
--- Add a new game place.
-- @param name The name of the map.
-- @param id The place id to teleport to.
function MatchmakingService:AddGamePlace(name: string, id: number)
self.GamePlaceIds[name] = id
if not self.PlayerRanges[name] then
self.PlayerRanges[name] = NumberRange.new(6, 10)
end
end
--- Sets whether or not this is a game server.
-- Disables match finding coroutine if it is.
-- @param newValue A boolean that indicates whether or not this server is a game server.
-- @param updateJoinableOnLeave A boolean that indicates whether or not to update the joinable status when a player leaves.
function MatchmakingService:SetIsGameServer(newValue: boolean, updateJoinableOnLeave: boolean)
self.IsGameServer = newValue
self.UpdateJoinableOnLeave = updateJoinableOnLeave
end
--- Sets the starting mean of OpenSkill objects.
-- Do not modify this unless you know what you're doing.
-- @param newStartingMean The new starting mean.
function MatchmakingService:SetStartingMean(newStartingMean: number)
self.StartingMean = newStartingMean
end
--- Sets the starting standard deviation of OpenSkill objects.
-- Do not modify this unless you know what you're doing.
-- @param newStartingStandardDeviation The new starting standing deviation.
function MatchmakingService:SetStartingStandardDeviation(newStartingStandardDeviation: number)
self.StartingStandardDeviation = newStartingStandardDeviation
end
--- Sets the max gap in rating between party members.
-- @param newMaxGap The new max gap between party members.
function MatchmakingService:SetMaxPartySkillGap(newMaxGap: number)
self.MaxPartySkillGap = newMaxGap
end
--- Sets the number of seconds between each queue expansion.
-- An expansion is 10 rounded skill level in each direction.
-- If a player is skill level 25, they get rounded to 30
-- @param newValue The new value, in seconds, of seconds between each queue expansion.
function MatchmakingService:SetSecondsBetweenExpansion(newValue: number)
self.SecondsPerExpansion = newValue
end
--- Sets the delay, in seconds, for players before being put into the teleport queue.
-- @param newValue The new value, in seconds, of the delay
function MatchmakingService:SetFoundGameDelay(newValue: number)
self.FoundGameDelay = newValue
end
--- Sets whether running games are joinable or not. If disabled, players will not be able to join running games and the matchmaking loop will completely skip running games.
-- @param newValue A boolean that indicates whether or not running games are joinable.
function MatchmakingService:SetRunningGamesJoinable(newValue: boolean)
self.RunningGamesJoinable = newValue
end
--- Clears all memory aside from player data.
function MatchmakingService:Clear()
print("Clearing memory")
local times = 0
local runningGames = MatchmakingService:GetAllRunningGames()
times += #runningGames
for _, v in ipairs(runningGames) do
if v.value.joinable then
times += 1
JoinableGamesMemory:RemoveAsync(v.key)
else
times += 1
NonJoinableGamesMemory:RemoveAsync(v.key)
end
end
MainMemory:RemoveAsync("QueuedSkillLevels")
MainMemory:RemoveAsync("MainJobId")
times += 2
local queuedMaps = getFromMemory(MemoryQueue, "QueuedMaps", 3)
times += 3
if queuedMaps == nil then return print("Total requests: " .. tostring(times)) end
MemoryQueue:RemoveAsync("QueuedMaps")
times += 1
for i, map in ipairs(queuedMaps) do
local mapQueue = self:GetQueue(map)
times += 1
MemoryQueue:RemoveAsync(map.."__QueuedRatingTypes")
if mapQueue == nil then continue end
for ratingType, skillLevelAndQueue in pairs(mapQueue) do
for skillLevel, queue in pairs(skillLevelAndQueue) do
times += 1
MemoryQueue:RemoveAsync(map.."__"..ratingType.."__"..skillLevel)
end
end
end
print("Total requests: " .. tostring(times))
end
function MatchmakingService.new(options: { MajorVersion: string | nil; DisableRatingSystem: boolean | nil; DisableExpansions: boolean | nil; DisableGlobalEvents: boolean | nil;} | nil)
local Service = {}
setmetatable(Service, MatchmakingService)
Service.Options = options or {}
Service.MatchmakingInterval = 3
Service.PlayerRanges = {}
Service.GamePlaceIds = {}
Service.IsGameServer = false
Service.UpdateJoinableOnLeave = false
Service.MaxPartySkillGap = 50
Service.PlayerAddedToQueue = Signal:Create()
Service.PlayerRemovedFromQueue = Signal:Create()
Service.FoundGame = Signal:Create()
Service.GameCreated = Signal:Create()
Service.FoundGameDelay = 0
Service.ApplyCustomTeleportData = nil
Service.ApplyGeneralTeleportData = nil
Service.SecondsPerExpansion = 10
Service.RunningGamesJoinable = true
-- Clears the store in studio
if RunService:IsStudio() then
task.spawn(Service.Clear, Service)
end
task.spawn(function()
local lastCheckMain = 0
local mainJobId = getFromMemory(MainMemory, "MainJobId", 3)
while not Service.IsGameServer and not CLOSED do
task.wait(Service.MatchmakingInterval)
local now = DateTime.now().UnixTimestampMillis
if lastCheckMain + 10000 <= now then
mainJobId = getFromMemory(MainMemory, "MainJobId", 3)
lastCheckMain = now
if mainJobId ~= nil and mainJobId[1] == game.JobId then
MainMemory:UpdateAsync("MainJobId", function(old)
if (old == nil or old[1] == game.JobId) and not CLOSED then
return {game.JobId, now}
end
return nil
end, 86400)
mainJobId = {game.JobId, now}
end
end
if mainJobId == nil or mainJobId[2] + 30000 <= now then
MainMemory:UpdateAsync("MainJobId", function(old)
if (old == nil or mainJobId == nil or old[1] == mainJobId[1]) and not CLOSED then
return {game.JobId, now}
end
return nil
end, 86400)
mainJobId = {game.JobId, now}
elseif mainJobId[1] == game.JobId then
-- Check all games for open slots
local parties = getFromMemory(MainMemory, "QueuedParties", 3)
if Service.RunningGamesJoinable then
local runningGames = Service:GetJoinableGames()
for _, g in ipairs(runningGames) do
local mem = g.value
-- Only try to add players to joinable games (sanity check)
if mem.joinable then
print("Checking running game")
print(mem)
-- Get the queue for this map, if there is no one left in the queue, skip it
local q = Service:GetQueue(mem.map)
if q == nil then
MemoryQueue:UpdateAsync("QueuedMaps", function(old)
if old == nil then return nil end
local index = table.find(old, mem.map)
if index == nil then return nil end
table.remove(old, index)
return old
end, 86400)
continue
end
-- Get the queue for this rating type, if there is no one left in the queue, skip it
local queue = q[mem.ratingType]
if queue == nil then continue end
local rolesToCheck = {}
for role, full in mem.full do
if not full then
table.insert(rolesToCheck, role)
end
end
for _, role in ipairs(rolesToCheck) do
-- Finally get the queue for the skill level and role, if there is no one left in the queue, skip it
local _queue = queue[tostring(mem.skillLevel)]
if _queue == nil then continue end
_queue = _queue[role]
if _queue == nil or #_queue == 0 then continue end
-- Get the number of expansions
local expansions = if not Service.Options.DisableExpansions then math.floor((now-mem.createTime)/(Service.SecondsPerExpansion*1000)) else 0
-- If there is no one queued at this skill level, and there are no expansions, skip it
if _queue == nil and expansions == 0 then continue end
-- Get the first values of the queue up to until the game is full
local values = first(_queue or {}, Service.PlayerRanges[mem.map][role].Max - #mem.players)
if not values or #values == 0 then continue end
-- Check for, and apply, expansions
if #values < Service.PlayerRanges[mem.map][role].Max - #mem.players then
for i = 1, expansions do
-- Skill difference is 10 per expansion in both directions
local skillUp = tostring(mem.skillLevel+10*i)
local skillDown = tostring(mem.skillLevel-10*i)
local queueUp = nil
local queueDown = nil
-- Get the queue at the rating type again
queueUp = q[mem.ratingType]
queueDown = q[mem.ratingType]
-- If there is anyone queued at the rating type, get the queue at the expanded skill level
if queueUp ~= nil then
if queueUp[skillUp] ~= nil then
queueUp = queueUp[skillUp][role]
end
if queueDown[skillDown] ~= nil then
queueDown = queueDown[skillDown][role]
end
end
-- Append these players to the end of the queue
append(values, queueUp)
append(values, queueDown)
-- Remove values that go past our necessary amount of players
if #values > Service.PlayerRanges[mem.map][role].Max - #mem.players then
for i = #values, Service.PlayerRanges[mem.map][role].Max - #mem.players, -1 do
table.remove(values, i)
end
break
end
end
end
-- If there is anyone, add them to the game
if values ~= nil then
-- Remove all newly queued players
for j = #values, 1, -1 do
if values[j][2] >= now - Service.MatchmakingInterval*1000 then
table.remove(values, j)
end
end
-- Remove parties that won't fit into the game and skip them
local acc = #values
while not checkForParties(values) do
local f = first(_queue, Service.PlayerRanges[mem.map][role].Max - #mem.players, acc + 1)
if f == nil or #f == 0 then
break
end
acc += #f
append(values, f)
end
end
-- If there are any players left, add them to the game
if values ~= nil and #values > 0 then
print("Adding to existing game")
local plrs = {}
local data = TeleportDataMemory:GetAsync(g.key) or {}
for _, v in ipairs(values) do
table.insert(plrs, v[1])
Service:SetPlayerInfoId(v[1], g.key, mem.ratingType, parties ~= nil and parties[v] or {}, mem.map, now + (Service.FoundGameDelay*1000))
end
Service:AddPlayersToGameId(plrs, g.key, role)
Service:RemovePlayersFromQueueId(tableSelect(values, 1), false)
if Service.ApplyCustomTeleportData ~= nil then
local gameData = Service.GetRunningGame(g.key)
for _, v in ipairs(plrs) do
data.customData[v] = Service.ApplyCustomTeleportData(Players:GetPlayerByUserId(v), gameData)
end
end
TeleportDataMemory:SetAsync(g.key, data, 86400)
end
end
end
end
end
-- Main matchmaking
-- Get all of the queued maps, if there are none, skip everything
local queuedMaps = getFromMemory(MemoryQueue, "QueuedMaps", 3)
if queuedMaps == nil then continue end
local playersAdded = {}
-- For every map, execute the matchmaking loop
for i, map in ipairs(queuedMaps) do
-- Get the queue for this map, if there is none, skip it
local mapQueue = Service:GetQueue(map)
if mapQueue == nil then
MemoryQueue:UpdateAsync("QueuedMaps", function(old)
if old == nil then return nil end
local index = table.find(old, map)
if index == nil then return nil end
table.remove(old, index)
return old
end, 86400)
continue
end
-- For every rating type queued...
for ratingType, skillLevelAndRoleQueue in pairs(mapQueue) do
-- For every skill level queued...
for skillLevel, roleQueue in pairs(skillLevelAndRoleQueue) do
local toCreateAGame = {}
-- For every role queued...
for role, queue in pairs (roleQueue) do
print(queue)
-- Get up to the maximum number of players for this map and role from the queue
local values = first(queue, Service.PlayerRanges[map][role].Max)
if not values or #values == 0 then continue end
-- Get any expansions to the queue
local expansions = if not Service.Options.DisableExpansions then math.floor((now-values[1][2])/(Service.SecondsPerExpansion*1000)) else 0
-- Check for, and apply, expansions
if values == nil or #values < Service.PlayerRanges[map][role].Max then
for i = 1, expansions do
-- Skill difference is 10 per expansion in both directions
local skillUp = tostring((tonumber(skillLevel) :: number)+10*i)
local skillDown = tostring((tonumber(skillLevel) :: number)-10*i)
local queueUp = nil
local queueDown = nil
-- Get the queue at the rating type
queueUp = mapQueue[ratingType]
queueDown = mapQueue[ratingType]
-- If there is anyone queued at the rating type, get the queue at the expanded skill level
if queueUp ~= nil then
if queueUp[skillUp] ~= nil then
queueUp = queueUp[skillUp][role]
end
if queueDown[skillDown] ~= nil then
queueDown = queueDown[skillDown][role]
end
end
-- Append these players to the end of the queue
if values == nil then values = {} end
append(values, queueUp)
append(values, queueDown)
end
-- Remove values that go past our necessary amount of players
if #values > Service.PlayerRanges[map][role].Max then
for i = #values, Service.PlayerRanges[map][role].Max, -1 do
table.remove(values, i)
end
break
end
end
if values ~= nil then
-- Remove all newly queued players
for j = #values, 1, -1 do
if values[j][2] >= now - Service.MatchmakingInterval*1000 then
table.remove(values, j)
end
end
-- Remove parties that won't fit into the game and skip them
local acc = #values
while not checkForParties(values) do
local f = first(queue, Service.PlayerRanges[map][role].Max, acc + 1)
if f == nil or #f == 0 then
break
end
acc += #f
append(values, f)
end
-- Remove players that already found a game (prevents double games with expansions)
for j = #values, 1, -1 do
if table.find(playersAdded, values[j][1]) then
table.remove(values, j)
end
end
end
-- If there aren't enough players than skip this skill level
if values == nil or #values < Service.PlayerRanges[map][role].Min then
continue
else
-- Get only the user ids from the players and add all of them to the playersAdded table
local userIds = tableSelect(values, 1)
append(playersAdded, userIds)
toCreateAGame[role] = userIds
end
end
-- Create a game with the players we found
local enoughToMakeGame = true
for role, range in pairs(Service.PlayerRanges[map]) do
if toCreateAGame[role] == nil or #toCreateAGame[role] < range.Min then
enoughToMakeGame = false
break
end
end
if enoughToMakeGame then
print("WE GOT ENOUGH TO MAKE GAME!!!")
print(toCreateAGame)
-- Reserve a server and tell all servers the player is ready to join
local reservedCode, serverId
if RunService:IsStudio() then
reservedCode = "TEST"
serverId = "TEST"
else
reservedCode, serverId = TeleportService:ReserveServer(Service.GamePlaceIds[map])
end
local gameData = {
["full"] = {};
["skillLevel"] = tonumber(skillLevel);
["players"] = toCreateAGame;
["started"] = false;
["joinable"] = Service.RunningGamesJoinable;
["ratingType"] = ratingType;
["map"] = map;
["createTime"] = now;
}
print("GAME DATA CREATED")
for role, range in pairs(Service.PlayerRanges[map]) do
gameData.full[role] = toCreateAGame[role] and #toCreateAGame[role] >= range.Max
end
if gameData.joinable then
local allFull = true
for role, full in pairs(gameData.full) do
if not full then
allFull = false
break
end
end
if allFull then
gameData.joinable = false
end
end
local success, err
success, err = pcall(function()
if gameData.joinable then
print("Adding to joinable")
JoinableGamesMemory:UpdateAsync(reservedCode, function()
return gameData
end, 86400)
else
print("Adding to non-joinable")
NonJoinableGamesMemory:UpdateAsync(reservedCode, function()
return gameData
end, 86400)
end
end)
print("GAME DATA ADDED")
if not success then
print("Error adding new game:")
warn(err)
else
success, err = pcall(function()
MainMemory:SetAsync(serverId, reservedCode, 86400)
end)
if not success then
print("Error adding new game:")
warn(err)
continue
end
print("Added game")
Service.GameCreated:Fire(gameData, serverId, reservedCode)
local userIds = {}
for role, plrs in pairs(toCreateAGame) do
for _, v in ipairs(plrs) do
table.insert(userIds, v)
Service:SetPlayerInfoId(v, reservedCode, ratingType, parties ~= nil and parties[tostring(v)] or {}, map, now + (Service.FoundGameDelay*1000))
end
end
--Service:RemoveExpansions(ratingType, skillLevel)
Service:RemovePlayersFromQueueId(userIds, false)
end
end
end
end
end
end
-- Teleport any players to their respective games
local playersToTeleport = {}
local playersToRatings = {}
local playersToMaps = {}
local gameCodesToData = {}
for _, v in ipairs(Players:GetPlayers()) do
if not v:GetAttribute("MMS_QUEUED") then
continue
end
local playerData = Service:GetPlayerInfoId(v.UserId)
if playerData ~= nil and now >= playerData.teleportAfter then
if not playerData.teleported and playerData.curGame ~= nil then
local gameData = gameCodesToData[playerData.curGame]
if not gameData then
gameData = Service:GetRunningGame(playerData.curGame)
gameCodesToData[playerData.curGame] = gameData
end
Service.FoundGame:Fire(v.UserId, playerData.curGame, gameData)
if playersToTeleport[playerData.curGame] == nil then playersToTeleport[playerData.curGame] = {} end
table.insert(playersToTeleport[playerData.curGame], v)
playersToRatings[v.UserId] = playerData.ratingType
playersToMaps[v.UserId] = playerData.map
MainMemory:UpdateAsync(v.UserId, function(old)
if old then
old.teleported = true
end
return old
end, 7200)
end
end
end
print("Players to teleport:")
print(playersToTeleport)
for code, players in pairs(playersToTeleport) do
local data = {gameCode=code, ratingType=playersToRatings[players[1].UserId], customData={}}
local gameData = gameCodesToData[code]
if Service.ApplyCustomTeleportData ~= nil then
for i, player in ipairs(players) do
data.customData[player.UserId] = Service.ApplyCustomTeleportData(player, gameData)
end
end
if Service.ApplyGeneralTeleportData ~= nil then
data.gameData = Service.ApplyGeneralTeleportData(gameData)
end
TeleportDataMemory:SetAsync(code, data, 86400)
print("Teleporting:")
print(players)
if code ~= "TEST" then TeleportService:TeleportToPrivateServer(Service.GamePlaceIds[playersToMaps[players[1].UserId]], code, players, nil) end
end
end
end)
return Service
end
--- Gets the current game's code. This only works on game servers!
-- @return The game's code, or nil if it isn't a game server.
function MatchmakingService:GetCurrentGameCode(): string?
if not self.IsGameServer or not game.PrivateServerId then
return nil
end
if not _gameId then
_gameId = MainMemory:GetAsync(game.PrivateServerId)
end
return _gameId
end
--- Gets a running game's data. This includes its code, ratingType,
-- and any general game data you applied through the ApplyGeneralTeleportData function.
-- This will not return custom player data, for that use GetUserData(player).