-
Notifications
You must be signed in to change notification settings - Fork 76
/
ygopro-server.coffee
3975 lines (3661 loc) · 162 KB
/
ygopro-server.coffee
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
# 标准库
net = require 'net'
http = require 'http'
url = require 'url'
path = require 'path'
fs = require 'fs'
os = require 'os'
crypto = require 'crypto'
exec = require('child_process').exec
execFile = require('child_process').execFile
spawn = require('child_process').spawn
spawnSync = require('child_process').spawnSync
_async = require('async')
# 三方库
_ = global._ = require 'underscore'
_.str = require 'underscore.string'
_.mixin(_.str.exports())
request = require 'request'
qs = require "querystring"
zlib = require 'zlib'
axios = require 'axios'
osu = require 'node-os-utils'
bunyan = require 'bunyan'
log = global.log = bunyan.createLogger name: "mycard"
moment = global.moment = require 'moment'
moment.updateLocale('zh-cn', {
relativeTime: {
future: '%s内',
past: '%s前',
s: '%d秒',
m: '1分钟',
mm: '%d分钟',
h: '1小时',
hh: '%d小时',
d: '1天',
dd: '%d天',
M: '1个月',
MM: '%d个月',
y: '1年',
yy: '%d年'
}
})
import_datas = global.import_datas = [
"abuse_count",
"ban_mc",
"vpass",
"rag",
"rid",
"is_post_watcher",
"retry_count",
"name",
"pass",
"name_vpass",
"is_first",
"lp",
"card_count",
"is_host",
"pos",
"surrend_confirm",
"kick_count",
"deck_saved",
"main",
"side",
"side_interval",
"side_tcount",
"selected_preduel",
"last_game_msg",
"last_game_msg_title",
"last_hint_msg",
"start_deckbuf",
"challonge_info",
"ready_trap",
"join_time",
"arena_quit_free",
"replays_sent"
]
merge = require 'deepmerge'
loadJSON = require('load-json-file').sync
loadJSONAsync = require('load-json-file')
util = require("util")
Q = require("q")
#heapdump = require 'heapdump'
checkFileExists = (path) =>
try
await fs.promises.access(path)
return true
catch e
return false
createDirectoryIfNotExists = (dirPath) =>
try
if dirPath and !await checkFileExists(dirPath)
await fs.promises.mkdir(dirPath, {recursive: true})
catch e
log.warn("Failed to create directory #{path}: #{e.toString()}")
setting_save = global.setting_save = (settings) ->
try
await fs.promises.writeFile(settings.file, JSON.stringify(settings, null, 2))
catch e
log.warn("setting save fail", e.toString())
return
setting_get = global.setting_get = (settings, path) ->
path = path.split(':')
if path.length == 0
return settings[path[0]]
else
target = settings
while path.length > 1
key = path.shift()
target = target[key]
key = path.shift()
return target[key]
setting_change = global.setting_change = (settings, path, val, noSave) ->
# path should be like "modules:welcome"
log.info("setting changed", path, val) if _.isString(val)
path=path.split(':')
if path.length == 0
settings[path[0]]=val
else
target=settings
while path.length > 1
key=path.shift()
target=target[key]
key = path.shift()
target[key] = val
if !noSave
await setting_save(settings)
return
importOldConfig = () ->
try
oldconfig=await loadJSONAsync('./config.user.json')
if oldconfig.tips
oldtips = {}
oldtips.file = './config/tips.json'
oldtips.tips = oldconfig.tips
await fs.promises.writeFile(oldtips.file, JSON.stringify(oldtips, null, 2))
delete oldconfig.tips
if oldconfig.dialogues
olddialogues = {}
olddialogues.file = './config/dialogues.json'
olddialogues.dialogues = oldconfig.dialogues
await fs.promises.writeFile(olddialogues.file, JSON.stringify(olddialogues, null, 2))
delete oldconfig.dialogues
oldbadwords={}
if oldconfig.ban
if oldconfig.ban.badword_level0
oldbadwords.level0 = oldconfig.ban.badword_level0
if oldconfig.ban.badword_level1
oldbadwords.level1 = oldconfig.ban.badword_level1
if oldconfig.ban.badword_level2
oldbadwords.level2 = oldconfig.ban.badword_level2
if oldconfig.ban.badword_level3
oldbadwords.level3 = oldconfig.ban.badword_level3
if not _.isEmpty(oldbadwords)
oldbadwords.file = './config/badwords.json'
await fs.promises.writeFile(oldbadwords.file, JSON.stringify(oldbadwords, null, 2))
delete oldconfig.ban.badword_level0
delete oldconfig.ban.badword_level1
delete oldconfig.ban.badword_level2
delete oldconfig.ban.badword_level3
if not _.isEmpty(oldconfig)
# log.info oldconfig
await fs.promises.writeFile('./config/config.json', JSON.stringify(oldconfig, null, 2))
log.info 'imported old config from config.user.json'
await fs.promises.rename('./config.user.json', './config.user.bak')
catch e
log.info e unless e.code == 'ENOENT'
auth = global.auth = require './ygopro-auth.js'
ygopro = global.ygopro = require './ygopro.js'
roomlist = null
settings = {}
tips = null
dialogues = null
badwords = null
badwordR = null
lflists = global.lflists = []
real_windbot_server_ip = null
long_resolve_cards = []
ReplayParser = null
athleticChecker = null
users_cache = {}
geoip = null
dataManager = null
windbots = []
disconnect_list = {} # {old_client, old_server, room_id, timeout, deckbuf}
extra_mode_list = global.extra_mode_list = [] # (rule) => void, with 'this' is ROOM
moment_now = global.moment_now = null
moment_now_string = global.moment_now_string = null
moment_long_ago_string = global.moment_long_ago_string = null
rooms_count = 0
challonge = null
class ResolveData
constructor: (@func) ->
resolved: false
resolve: (err, data) ->
if @resolved
return false
@resolved = true
@func(err, data)
return true
loadLFList = (path) ->
try
for list in (await fs.promises.readFile(path, 'utf8')).match(/!.*/g)
date=list.match(/!([\d\.]+)/)
continue unless date
lflists.push({date: moment(list.match(/!([\d\.]+)/)[1], 'YYYY.MM.DD').utcOffset("-08:00"), tcg: list.indexOf('TCG') != -1})
catch
init = () ->
log.info('Reading config.')
await createDirectoryIfNotExists("./config")
await importOldConfig()
defaultConfig = await loadJSONAsync('./data/default_config.json')
if await checkFileExists("./config/config.json")
try
config = await loadJSONAsync('./config/config.json')
catch e
console.error("Failed reading config: ", e.toString())
process.exit(1)
else
config = {}
settings = global.settings = merge(defaultConfig, config, { arrayMerge: (destination, source) -> source })
#import old configs
imported = false
#reset http.quick_death_rule from true to 1
if settings.modules.http.quick_death_rule == true
settings.modules.http.quick_death_rule = 1
imported = true
else if settings.modules.http.quick_death_rule == false
settings.modules.http.quick_death_rule = 2
imported = true
#import the old passwords to new admin user system
if settings.modules.http.password
log.info('Migrating http user.')
await auth.add_user("olduser", settings.modules.http.password, true, {
"get_rooms": true,
"shout": true,
"stop": true,
"change_settings": true,
"ban_user": true,
"kick_user": true,
"start_death": true
})
delete settings.modules.http.password
imported = true
if settings.modules.tournament_mode.password
log.info('Migrating tournament user.')
await auth.add_user("tournament", settings.modules.tournament_mode.password, true, {
"duel_log": true,
"download_replay": true,
"clear_duel_log": true,
"deck_dashboard_read": true,
"deck_dashboard_write": true,
})
delete settings.modules.tournament_mode.password
imported = true
if settings.modules.pre_util.password
log.info('Migrating pre-dash user.')
await auth.add_user("pre", settings.modules.pre_util.password, true, {
"pre_dashboard": true
})
delete settings.modules.pre_util.password
imported = true
if settings.modules.update_util.password
log.info('Migrating update-dash user.')
await auth.add_user("update", settings.modules.update_util.password, true, {
"update_dashboard": true
})
delete settings.modules.update_util.password
imported = true
#import the old enable_priority hostinfo
if settings.hostinfo.enable_priority or settings.hostinfo.enable_priority == false
if settings.hostinfo.enable_priority
settings.hostinfo.duel_rule = 3
else
settings.hostinfo.duel_rule = 5
delete settings.hostinfo.enable_priority
imported = true
#import the old Challonge api key option
if settings.modules.challonge.options
settings.modules.challonge.api_key = settings.modules.challonge.options.apiKey
delete settings.modules.challonge.options
imported = true
#import the old random_duel.blank_pass_match option
if settings.modules.random_duel.blank_pass_match == true
settings.modules.random_duel.blank_pass_modes = {"S":true,"M":true,"T":false}
delete settings.modules.random_duel.blank_pass_match
imported = true
if settings.modules.random_duel.blank_pass_match == false
settings.modules.random_duel.blank_pass_modes = {"S":true,"M":false,"T":false}
delete settings.modules.random_duel.blank_pass_match
imported = true
if settings.modules.hide_name == true
settings.modules.hide_name = "start"
imported = true
#finish
keysFromEnv = Object.keys(process.env).filter((key) => key.startsWith('SRVPRO_'))
if keysFromEnv.length > 0
log.info('Migrating settings from environment variables.')
for key in keysFromEnv
settingKey = key.slice(7).replace(/__/g, ':')
val = process.env[key]
valFromDefault = setting_get(defaultConfig, settingKey)
if Array.isArray(valFromDefault)
val = val.split(',')
valFromDefault = valFromDefault[0]
if typeof valFromDefault == 'number'
val = parseFloat(val)
if typeof valFromDefault == 'boolean'
val = (val != 'false') && (val != '0')
setting_change(settings, settingKey, val, true)
imported = true
if imported
log.info('Saving migrated settings.')
await setting_save(settings)
if settings.modules.mysql.enabled
global.PrimaryKeyType = if settings.modules.mysql.db.type == 'sqlite' then 'integer' else 'bigint'
DataManager = require('./data-manager/DataManager.js').DataManager
dataManager = global.dataManager = new DataManager(settings.modules.mysql.db, log)
log.info('Connecting to database.')
await dataManager.init()
else
log.warn("Some functions may be limited without MySQL .")
if settings.modules.cloud_replay.enabled
settings.modules.cloud_replay.enabled = false
await setting_save(settings)
log.warn("Cloud replay cannot be enabled because no MySQL.")
if settings.modules.tournament_mode.enable_recover
settings.modules.tournament_mode.enable_recover = false
await setting_save(settings)
log.warn("Recover mode cannot be enabled because no MySQL.")
if settings.modules.chat_color.enabled
settings.modules.chat_color.enabled = false
await setting_save(settings)
log.warn("Chat color cannot be enabled because no MySQL.")
if settings.modules.random_duel.record_match_scores
settings.modules.random_duel.record_match_scores = false
await setting_save(settings)
log.warn("Cannot record random match scores because no MySQL.")
# 读取数据
log.info('Loading data.')
default_data = await loadJSONAsync('./data/default_data.json')
try
tips = global.tips = await loadJSONAsync('./config/tips.json')
catch
tips = global.tips = default_data.tips
await setting_save(tips)
try
dialogues = global.dialogues = await loadJSONAsync('./config/dialogues.json')
catch
dialogues = global.dialogues = default_data.dialogues
await setting_save(dialogues)
try
badwords = global.badwords = await loadJSONAsync('./config/badwords.json')
catch
badwords = global.badwords = default_data.badwords
await setting_save(badwords)
if settings.modules.chat_color.enabled and await checkFileExists('./config/chat_color.json')
try
chat_color = await loadJSONAsync('./config/chat_color.json')
if chat_color
log.info("Migrating chat color.")
await dataManager.migrateChatColors(chat_color.save_list);
await fs.promises.rename('./config/chat_color.json', './config/chat_color.json.bak')
log.info("Chat color migrated.")
catch
try
log.info("Reading YGOPro version.")
cppversion = parseInt((await fs.promises.readFile('ygopro/gframe/game.cpp', 'utf8')).match(/PRO_VERSION = ([x\dABCDEF]+)/)[1], '16')
await setting_change(settings, "version", cppversion)
log.info "ygopro version 0x"+settings.version.toString(16), "(from source code)"
catch
#settings.version = settings.version_default
log.info "ygopro version 0x"+settings.version.toString(16), "(from config)"
# load the lflist of current date
log.info("Reading banlists.")
await loadLFList('ygopro/expansions/lflist.conf')
await loadLFList('ygopro/lflist.conf')
badwordR = global.badwordR = {}
badwordR.level0=new RegExp('(?:'+badwords.level0.join(')|(?:')+')','i');
badwordR.level1=new RegExp('(?:'+badwords.level1.join(')|(?:')+')','i');
badwordR.level1g=new RegExp('(?:'+badwords.level1.join(')|(?:')+')','ig');
badwordR.level2=new RegExp('(?:'+badwords.level2.join(')|(?:')+')','i');
badwordR.level3=new RegExp('(?:'+badwords.level3.join(')|(?:')+')','i');
setInterval ()->
moment_now = global.moment_now = moment()
moment_now_string = global.moment_now_string = moment_now.format()
moment_long_ago_string = global.moment_long_ago_string = moment().subtract(settings.modules.random_duel.hang_timeout - 19, 's').format()
return
, 500
if settings.modules.max_rooms_count
rooms_count=0
get_rooms_count = ()->
_rooms_count=0
for room in ROOM_all when room and room.established
_rooms_count++
rooms_count=_rooms_count
setTimeout get_rooms_count, 1000
return
setTimeout get_rooms_count, 1000
if settings.modules.windbot.enabled
log.info("Reading bot list.")
windbots = global.windbots = (await loadJSONAsync(settings.modules.windbot.botlist)).windbots
real_windbot_server_ip = global.real_windbot_server_ip = settings.modules.windbot.server_ip
if !settings.modules.windbot.server_ip.includes("127.0.0.1")
dns = require('dns')
real_windbot_server_ip = global.real_windbot_server_ip = await util.promisify(dns.lookup)(settings.modules.windbot.server_ip)
if settings.modules.heartbeat_detection.enabled
long_resolve_cards = global.long_resolve_cards = await loadJSONAsync('./data/long_resolve_cards.json')
if settings.modules.tournament_mode.enable_recover
ReplayParser = global.ReplayParser = require "./Replay.js"
if settings.modules.athletic_check.enabled
AthleticChecker = require("./athletic-check.js").AthleticChecker
athleticChecker = global.athleticChecker = new AthleticChecker(settings.modules.athletic_check)
if settings.modules.http.websocket_roomlist
roomlist = global.roomlist = require './roomlist.js'
if settings.modules.i18n.auto_pick
geoip = require('geoip-country-lite')
if settings.modules.mycard.enabled
pgClient = require('pg').Client
pg_client = global.pg_client = new pgClient(settings.modules.mycard.auth_database)
pg_client.on 'error', (err) ->
log.warn "PostgreSQL ERROR: ", err
return
pg_query = pg_client.query('SELECT username, id from users')
pg_query.on 'error', (err) ->
log.warn "PostgreSQL Query ERROR: ", err
return
pg_query.on 'row', (row) ->
#log.info "load user", row.username, row.id
users_cache[row.username] = row.id
return
pg_query.on 'end', (result) ->
log.info "users loaded", result.rowCount
return
pg_client.on 'drain', pg_client.end.bind(pg_client)
log.info "loading mycard user..."
pg_client.connect()
if settings.modules.arena_mode.enabled and settings.modules.arena_mode.init_post.enabled
postData = qs.stringify({
ak: settings.modules.arena_mode.init_post.accesskey,
arena: settings.modules.arena_mode.mode
})
try
log.info("Sending arena init post.")
await axios.post(settings.modules.arena_mode.init_post.url + "?" + postData)
catch e
log.warn 'ARENA INIT POST ERROR', e
if settings.modules.challonge.enabled
Challonge = require('./challonge').Challonge
challonge = new Challonge(settings.modules.challonge)
if settings.modules.tips.get
load_tips()
if settings.modules.tips.enabled
if settings.modules.tips.interval
setInterval ()->
for room in ROOM_all when room and room.established and room.duel_stage != ygopro.constants.END
ygopro.stoc_send_random_tip_to_room(room) if room.duel_stage != ygopro.constants.DUEL_STAGE.DUELING
return
, settings.modules.tips.interval
if settings.modules.tips.interval_ingame
setInterval ()->
for room in ROOM_all when room and room.established and room.duel_stage != ygopro.constants.END
ygopro.stoc_send_random_tip_to_room(room) if room.duel_stage == ygopro.constants.DUEL_STAGE.DUELING
return
, settings.modules.tips.interval_ingame
if settings.modules.dialogues.get
load_dialogues()
if settings.modules.random_duel.post_match_scores and settings.modules.mysql.enabled
setInterval(()->
scores = await dataManager.getRandomScoreTop10()
try
await axios.post(settings.modules.random_duel.post_match_scores, qs.stringify({
accesskey: settings.modules.random_duel.post_match_accesskey,
rank: JSON.stringify(scores)
}))
catch e
log.warn 'RANDOM SCORE POST ERROR', e.toString()
return
, 60000)
if settings.modules.random_duel.enabled
setInterval ()->
for room in ROOM_all when room and room.duel_stage != ygopro.constants.DUEL_STAGE.BEGIN and room.random_type and room.last_active_time and room.waiting_for_player and room.get_disconnected_count() == 0 and (!settings.modules.side_timeout or room.duel_stage != ygopro.constants.DUEL_STAGE.SIDING) and !room.recovered
time_passed = Math.floor(moment_now.diff(room.last_active_time) / 1000)
#log.info time_passed, moment_now_string
if time_passed >= settings.modules.random_duel.hang_timeout
room.refreshLastActiveTime()
await ROOM_ban_player(room.waiting_for_player.name, room.waiting_for_player.ip, "${random_ban_reason_AFK}")
room.scores[room.waiting_for_player.name_vpass] = -9
#log.info room.waiting_for_player.name, room.scores[room.waiting_for_player.name_vpass]
ygopro.stoc_send_chat_to_room(room, "#{room.waiting_for_player.name} ${kicked_by_system}", ygopro.constants.COLORS.RED)
CLIENT_send_replays_and_kick(room.waiting_for_player, room)
else if time_passed >= (settings.modules.random_duel.hang_timeout - 20) and not (time_passed % 10)
ygopro.stoc_send_chat_to_room(room, "#{room.waiting_for_player.name} ${afk_warn_part1}#{settings.modules.random_duel.hang_timeout - time_passed}${afk_warn_part2}", ygopro.constants.COLORS.RED)
ROOM_unwelcome(room, room.waiting_for_player, "${random_ban_reason_AFK}")
return
, 1000
if settings.modules.mycard.enabled
setInterval ()->
for room in ROOM_all when room and room.duel_stage != ygopro.constants.DUEL_STAGE.BEGIN and room.arena and room.last_active_time and room.waiting_for_player and room.get_disconnected_count() == 0 and (!settings.modules.side_timeout or room.duel_stage != ygopro.constants.DUEL_STAGE.SIDING) and !room.recovered
time_passed = Math.floor(moment_now.diff(room.last_active_time) / 1000)
#log.info time_passed
if time_passed >= settings.modules.random_duel.hang_timeout
room.refreshLastActiveTime()
ygopro.stoc_send_chat_to_room(room, "#{room.waiting_for_player.name} ${kicked_by_system}", ygopro.constants.COLORS.RED)
room.scores[room.waiting_for_player.name_vpass] = -9
#log.info room.waiting_for_player.name, room.scores[room.waiting_for_player.name_vpass]
CLIENT_send_replays_and_kick(room.waiting_for_player, room)
else if time_passed >= (settings.modules.random_duel.hang_timeout - 20) and not (time_passed % 10)
ygopro.stoc_send_chat_to_room(room, "#{room.waiting_for_player.name} ${afk_warn_part1}#{settings.modules.random_duel.hang_timeout - time_passed}${afk_warn_part2}", ygopro.constants.COLORS.RED)
if true # settings.modules.arena_mode.punish_quit_before_match
for room in ROOM_all when room and room.arena and room.duel_stage == ygopro.constants.DUEL_STAGE.BEGIN and room.get_playing_player().length < 2
player = room.get_playing_player()[0]
if player and player.join_time and !player.arena_quit_free
waited_time = moment_now.diff(player.join_time)
if waited_time >= 30000
ygopro.stoc_send_chat(player, "${arena_wait_timeout}", ygopro.constants.COLORS.BABYBLUE)
player.arena_quit_free = true
else if waited_time >= 5000 and waited_time < 6000
ygopro.stoc_send_chat(player, "${arena_wait_hint}", ygopro.constants.COLORS.BABYBLUE)
return
, 1000
if settings.modules.heartbeat_detection.enabled
setInterval ()->
for room in ROOM_all when room and room.duel_stage != ygopro.constants.DUEL_STAGE.BEGIN and (room.hostinfo.time_limit == 0 or room.duel_stage != ygopro.constants.DUEL_STAGE.DUELING) and !room.windbot
for player in room.get_playing_player() when player and (room.duel_stage != ygopro.constants.DUEL_STAGE.SIDING or player.selected_preduel)
CLIENT_heartbeat_register(player, true)
return
, settings.modules.heartbeat_detection.interval
if settings.modules.windbot.enabled and settings.modules.windbot.spawn
spawn_windbot()
setInterval ()->
for room in ROOM_all when room and room.duel_stage != ygopro.constants.DUEL_STAGE.BEGIN and room.hostinfo.auto_death and !room.auto_death_triggered and moment_now.diff(room.start_time) > 60000 * room.hostinfo.auto_death
room.auto_death_triggered = true
room.start_death()
, 1000
log.info("Starting server.")
net.createServer(netRequestHandler).listen settings.port, ->
log.info "server started", settings.port
return
if settings.modules.stop
log.info "NOTE: server not open due to config, ", settings.modules.stop
http_server = http.createServer(httpRequestListener)
main_http_server = http_server
if settings.modules.http.ssl.enabled
https = require 'https'
httpsOptions =
cert: await fs.promises.readFile(settings.modules.http.ssl.cert)
key: await fs.promises.readFile(settings.modules.http.ssl.key)
https_server = https.createServer(httpsOptions, httpRequestListener)
https_server.listen settings.modules.http.ssl.port
main_http_server = https_server
if settings.modules.http.websocket_roomlist and roomlist
roomlist.init main_http_server, ROOM_all
http_server.listen settings.modules.http.port
if settings.modules.neos.enabled
ws = require 'ws'
neosHttpServer = null
if settings.modules.http.ssl.enabled
neosHttpServer = https.createServer(httpsOptions)
else
neosHttpServer = http.createServer()
neosWsServer = new ws.WebSocketServer({server: neosHttpServer})
neosWsServer.on 'connection', neosRequestListener
neosHttpServer.listen settings.modules.neos.port
mkdirList = [
"./plugins",
settings.modules.tournament_mode.deck_path,
settings.modules.tournament_mode.replay_path,
settings.modules.tournament_mode.log_save_path,
settings.modules.deck_log.local
]
for dirPath in mkdirList
await createDirectoryIfNotExists(dirPath)
plugin_list = await fs.promises.readdir("./plugins")
for plugin_filename in plugin_list
if plugin_filename.endsWith '.js'
plugin_path = process.cwd() + "/plugins/" + plugin_filename
require(plugin_path)
log.info("Plugin loaded:", plugin_filename)
return
# 获取可用内存
memory_usage = global.memory_usage = 0
get_memory_usage = global.get_memory_usage = ()->
memoryInfo = await osu.mem.info()
percentUsed = 100 - memoryInfo.freeMemPercentage
# console.log(percentUsed)
memory_usage = global.memory_usage = percentUsed
return
get_memory_usage()
setInterval(get_memory_usage, 3000)
ROOM_all = global.ROOM_all = []
ROOM_players_oppentlist = global.ROOM_players_oppentlist = {}
ROOM_connected_ip = global.ROOM_connected_ip = {}
ROOM_bad_ip = global.ROOM_bad_ip = {}
# ban a user manually and permanently
ban_user = global.ban_user = (name) ->
if !settings.modules.mysql.enabled
throw "MySQL is not enabled"
bans = [dataManager.getBan(name, null)]
for room in ROOM_all when room and room.established
for playerType in ["players", "watchers"]
for player in room[playerType] when player.name == name or bans.find((ban) => player.ip == ban.ip)
bans.push(dataManager.getBan(name, player.ip))
ROOM_bad_ip[player.ip]=99
ygopro.stoc_send_chat_to_room(room, "#{player.name} ${kicked_by_system}", ygopro.constants.COLORS.RED)
CLIENT_send_replays_and_kick(player, room)
for ban in bans
await dataManager.banPlayer(ban)
return
# automatically ban user to use random duel
ROOM_ban_player = global.ROOM_ban_player = (name, ip, reason, countadd = 1)->
return if settings.modules.test_mode.no_ban_player or !settings.modules.mysql.enabled
await dataManager.randomDuelBanPlayer(ip, reason, countadd)
return
ROOM_kick = (name, callback)->
found = false
_async.each(ROOM_all, (room, done)->
if !(room and room.established and (name == "all" or name == room.process_pid.toString() or name == room.name))
done()
return
found = true
room.terminate()
done()
, (err)->
callback(null, found)
return
)
ROOM_player_win = global.ROOM_player_win = (name)->
if !settings.modules.mysql.enabled
return
await dataManager.randomDuelPlayerWin(name)
return
ROOM_player_lose = global.ROOM_player_lose = (name)->
if !settings.modules.mysql.enabled
return
await dataManager.randomDuelPlayerLose(name)
return
ROOM_player_flee = global.ROOM_player_flee = (name)->
if !settings.modules.mysql.enabled
return
await dataManager.randomDuelPlayerFlee(name)
return
ROOM_player_get_score = global.ROOM_player_get_score = (player, display_name)->
if !settings.modules.mysql.enabled
return ""
return await dataManager.getRandomDuelScoreDisplay(player.name_vpass, display_name)
ROOM_find_or_create_by_name = global.ROOM_find_or_create_by_name = (name, player_ip)->
uname=name.toUpperCase()
if settings.modules.windbot.enabled and (uname[0...2] == 'AI' or (!settings.modules.random_duel.enabled and uname == ''))
return ROOM_find_or_create_ai(name)
if settings.modules.random_duel.enabled and (uname == '' or uname == 'S' or uname == 'M' or uname == 'T')
return await ROOM_find_or_create_random(uname, player_ip)
if room = ROOM_find_by_name(name)
return room
else if memory_usage >= 90 or (settings.modules.max_rooms_count and rooms_count >= settings.modules.max_rooms_count)
return null
else
room = new Room(name)
if room.recover_duel_log_id
success = await room.initialize_recover()
if !success
return {"error": "${cloud_replay_no}"}
return room
ROOM_find_or_create_random = global.ROOM_find_or_create_random = (type, player_ip)->
if settings.modules.mysql.enabled
randomDuelBanRecord = await dataManager.getRandomDuelBan(player_ip)
if randomDuelBanRecord
if randomDuelBanRecord.count > 6 and moment_now.isBefore(randomDuelBanRecord.time)
return {"error": "${random_banned_part1}#{randomDuelBanRecord.reasons.join('${random_ban_reason_separator}')}${random_banned_part2}#{moment(randomDuelBanRecord.time).fromNow(true)}${random_banned_part3}"}
if randomDuelBanRecord.count > 3 and moment_now.isBefore(randomDuelBanRecord.time) and randomDuelBanRecord.getNeedTip() and type != 'T'
randomDuelBanRecord.setNeedTip(false)
await dataManager.updateRandomDuelBan(randomDuelBanRecord)
return {"error": "${random_deprecated_part1}#{randomDuelBanRecord.reasons.join('${random_ban_reason_separator}')}${random_deprecated_part2}#{moment(randomDuelBanRecord.time).fromNow(true)}${random_deprecated_part3}"}
else if randomDuelBanRecord.getNeedTip()
randomDuelBanRecord.setNeedTip(false)
await dataManager.updateRandomDuelBan(randomDuelBanRecord)
return {"error": "${random_warn_part1}#{randomDuelBanRecord.reasons.join('${random_ban_reason_separator}')}${random_warn_part2}"}
else if randomDuelBanRecord.count > 2
randomDuelBanRecord.setNeedTip(true)
await dataManager.updateRandomDuelBan(randomDuelBanRecord)
max_player = if type == 'T' then 4 else 2
playerbanned = (randomDuelBanRecord and randomDuelBanRecord.count > 3 and moment_now < randomDuelBanRecord.time)
result = _.find ROOM_all, (room)->
return room and room.random_type != '' and !room.disconnector and room.duel_stage == ygopro.constants.DUEL_STAGE.BEGIN and !room.windbot and
((type == '' and
(room.random_type == settings.modules.random_duel.default_type or
settings.modules.random_duel.blank_pass_modes[room.random_type])) or
room.random_type == type) and
0 < room.get_playing_player().length < max_player and
(settings.modules.random_duel.no_rematch_check or room.get_host() == null or
room.get_host().ip != ROOM_players_oppentlist[player_ip]) and
(playerbanned == room.deprecated or type == 'T')
if result
result.welcome = '${random_duel_enter_room_waiting}'
#log.info 'found room', player_name
else if memory_usage < 90 and not (settings.modules.max_rooms_count and rooms_count >= settings.modules.max_rooms_count)
type = if type then type else settings.modules.random_duel.default_type
name = type + ',RANDOM#' + Math.floor(Math.random() * 100000)
result = new Room(name)
result.random_type = type
result.max_player = max_player
result.welcome = '${random_duel_enter_room_new}'
result.deprecated = playerbanned
#log.info 'create room', player_name, name
else
return null
if result.random_type=='S' then result.welcome2 = '${random_duel_enter_room_single}'
if result.random_type=='M' then result.welcome2 = '${random_duel_enter_room_match}'
if result.random_type=='T' then result.welcome2 = '${random_duel_enter_room_tag}'
return result
ROOM_find_or_create_ai = global.ROOM_find_or_create_ai = (name)->
if name == ''
name = 'AI'
namea = name.split('#')
uname = name.toUpperCase()
if room = ROOM_find_by_name(name)
return room
else if uname == 'AI'
windbot = _.sample _.filter windbots, (w)->
!w.hidden
name = 'AI#' + Math.floor(Math.random() * 100000)
else if namea.length>1
ainame = namea[namea.length-1]
windbot = _.sample _.filter windbots, (w)->
w.name == ainame or w.deck == ainame
if !windbot
return { "error": "${windbot_deck_not_found}" }
name = namea[0].toUpperCase() + '#N' + Math.floor(Math.random() * 100000)
else
windbot = _.sample _.filter windbots, (w)->
!w.hidden
name = name + '#' + Math.floor(Math.random() * 10000)
if name.replace(/[^\x00-\xff]/g,"00").length>20
log.info "long ai name", name
return { "error": "${windbot_name_too_long}" }
result = new Room(name)
result.windbot = windbot
result.private = true
return result
ROOM_find_by_name = global.ROOM_find_by_name = (name)->
result = _.find ROOM_all, (room)->
return room and room.name == name
return result
ROOM_find_by_title = global.ROOM_find_by_title = (title)->
result = _.find ROOM_all, (room)->
return room and room.title == title
return result
ROOM_find_by_port = global.ROOM_find_by_port = (port)->
_.find ROOM_all, (room)->
return room and room.port == port
ROOM_find_by_pid = global.ROOM_find_by_pid = (pid)->
_.find ROOM_all, (room)->
return room and room.process_pid == pid
ROOM_validate = global.ROOM_validate = (name)->
client_name_and_pass = name.split('$', 2)
client_name = client_name_and_pass[0]
client_pass = client_name_and_pass[1]
return true if !client_pass
!_.find ROOM_all, (room)->
return false unless room
room_name_and_pass = room.name.split('$', 2)
room_name = room_name_and_pass[0]
room_pass = room_name_and_pass[1]
client_name == room_name and client_pass != room_pass
ROOM_unwelcome = global.ROOM_unwelcome = (room, bad_player, reason)->
return unless room
for player in room.players
if player and player == bad_player
ygopro.stoc_send_chat(player, "${unwelcome_warn_part1}#{reason}${unwelcome_warn_part2}", ygopro.constants.COLORS.RED)
else if player and player.pos!=7 and player != bad_player
player.flee_free=true
ygopro.stoc_send_chat(player, "${unwelcome_tip_part1}#{reason}${unwelcome_tip_part2}", ygopro.constants.COLORS.BABYBLUE)
return
CLIENT_kick = global.CLIENT_kick = (client) ->
if !client
return false
client.system_kicked = true
if settings.modules.reconnect.enabled and client.isClosed
if client.server and !client.had_new_reconnection
room = ROOM_all[client.rid]
if room
room.disconnect(client)
else
SERVER_kick(client.server)
else
client.destroy()
return true
SERVER_kick = global.SERVER_kick = (server) ->
if !server
return false
server.system_kicked = true
server.destroy()
return true
release_disconnect = global.release_disconnect = (dinfo, reconnected) ->
if dinfo.old_client and !reconnected
dinfo.old_client.destroy()
if dinfo.old_server and !reconnected
SERVER_kick(dinfo.old_server)
clearTimeout(dinfo.timeout)
return
CLIENT_get_authorize_key = global.CLIENT_get_authorize_key = (client) ->
if !settings.modules.mycard.enabled and client.vpass
return client.name_vpass
else if settings.modules.mycard.enabled or settings.modules.tournament_mode.enabled or settings.modules.challonge.enabled or client.is_local
return client.name
else
return client.ip + ":" + client.name
CLIENT_reconnect_unregister = global.CLIENT_reconnect_unregister = (client, reconnected, exact) ->
if !settings.modules.reconnect.enabled
return false
if disconnect_list[CLIENT_get_authorize_key(client)]
if exact and disconnect_list[CLIENT_get_authorize_key(client)].old_client != client
return false
release_disconnect(disconnect_list[CLIENT_get_authorize_key(client)], reconnected)
delete disconnect_list[CLIENT_get_authorize_key(client)]
return true
return false
CLIENT_reconnect_register = global.CLIENT_reconnect_register = (client, room_id, error) ->
room = ROOM_all[room_id]
if client.had_new_reconnection
return false
if !settings.modules.reconnect.enabled or !room or client.system_kicked or client.flee_free or disconnect_list[CLIENT_get_authorize_key(client)] or client.is_post_watcher or !CLIENT_is_player(client, room) or room.duel_stage == ygopro.constants.DUEL_STAGE.BEGIN or room.windbot or (settings.modules.reconnect.auto_surrender_after_disconnect and room.hostinfo.mode != 1) or (room.random_type and room.get_disconnected_count() > 1)
return false
# for player in room.players
# if player != client and CLIENT_get_authorize_key(player) == CLIENT_get_authorize_key(client)
# return false # some issues may occur in this case, so return false
dinfo = {
room_id: room_id,
old_client: client,
old_server: client.server,
deckbuf: client.start_deckbuf
}
tmot = setTimeout(() ->
room.disconnect(client, error)
#SERVER_kick(dinfo.old_server)
return
, settings.modules.reconnect.wait_time)
dinfo.timeout = tmot
disconnect_list[CLIENT_get_authorize_key(client)] = dinfo
#console.log("#{client.name} ${disconnect_from_game}")
ygopro.stoc_send_chat_to_room(room, "#{room.getMaskedPlayerName(client)} ${disconnect_from_game}" + if error then ": #{error}" else '')
if client.time_confirm_required
client.time_confirm_required = false
ygopro.ctos_send(client.server, 'TIME_CONFIRM')
if settings.modules.reconnect.auto_surrender_after_disconnect and room.duel_stage == ygopro.constants.DUEL_STAGE.DUELING
ygopro.ctos_send(client.server, 'SURRENDER')
return true
CLIENT_import_data = global.CLIENT_import_data = (client, old_client, room) ->
for player,index in room.players
if player == old_client
room.players[index] = client
break
room.dueling_players[old_client.pos] = client
if room.waiting_for_player == old_client
room.waiting_for_player = client
if room.waiting_for_player2 == old_client
room.waiting_for_player2 = client
if room.selecting_tp == old_client
room.selecting_tp = client
if room.determine_firstgo == old_client
room.determine_firstgo = client
for key in import_datas
client[key] = old_client[key]
old_client.had_new_reconnection = true
return
SERVER_clear_disconnect = global.SERVER_clear_disconnect = (server) ->
return false unless settings.modules.reconnect.enabled
for k,v of disconnect_list
if v and server == v.old_server
release_disconnect(v)
delete disconnect_list[k]
return true
return false
ROOM_clear_disconnect = global.ROOM_clear_disconnect = (room_id) ->
return false unless settings.modules.reconnect.enabled
for k,v of disconnect_list
if v and room_id == v.room_id
release_disconnect(v)
delete disconnect_list[k]
return true
return false
CLIENT_is_player = global.CLIENT_is_player = (client, room) ->
is_player = false
for player in room.players
if client == player
is_player = true
break
return is_player and client.pos <= 3
CLIENT_is_able_to_reconnect = global.CLIENT_is_able_to_reconnect = (client, deckbuf) ->
unless settings.modules.reconnect.enabled
return false
if client.system_kicked
return false
disconnect_info = disconnect_list[CLIENT_get_authorize_key(client)]
unless disconnect_info and disconnect_info.deckbuf
return false
room = ROOM_all[disconnect_info.room_id]
if !room
CLIENT_reconnect_unregister(client)
return false
if deckbuf and deckbuf.compare(disconnect_info.deckbuf) != 0
return false
return true
CLIENT_get_kick_reconnect_target = global.CLIENT_get_kick_reconnect_target = (client, deckbuf) ->
for room in ROOM_all when room and room.duel_stage != ygopro.constants.DUEL_STAGE.BEGIN and !room.windbot
for player in room.get_playing_player() when !player.isClosed and player.name == client.name and (settings.modules.challonge.enabled or player.pass == client.pass) and (settings.modules.mycard.enabled or settings.modules.tournament_mode.enabled or player.ip == client.ip or (client.vpass and client.vpass == player.vpass)) and (!deckbuf or deckbuf.compare(player.start_deckbuf) == 0)
return player
return null
CLIENT_is_able_to_kick_reconnect = global.CLIENT_is_able_to_kick_reconnect = (client, deckbuf) ->