Во-первых, здание оружейного городка
задний план
Недавно босс полюбил есть курицу (мобильная игра: All-Army Attack) и часто таскал нас погонять в темноте, ему оставалось только отказаться от обеденного перерыва и сопровождать босса, чтобы побегать по пустыне. На прошлой неделе, когда я смотрел запись на игровом канале WeChat, у меня возникла прихоть, смогу ли я таким образом зафиксировать много данных о битвах, а затем проанализировать их, чтобы увидеть, каковы правила.
Покажите волну рекордов, наша команда имеет очень высокий показатель поедания цыплят в случае с черными, 51 раз в почти 100 играх.
После краткой оценки, если это представляется возможным, давайте начнем.
Шаг 1 Анализ интерфейса данных
Первым шагом, конечно же, является сбор этих данных записи, прежде всего нам нужно понять историю, стоящую за страницей. Зайдите и посмотрите, как на странице появляется боевая статистика.
Используйте Чарльза для захвата пакетов
Реализация захвата пакетов
Рекомендуется использовать инструмент Charles для захвата трафика на мобильном телефоне с уровня протокола под Mac.Принцип заключается в том, чтобы открыть прокси-сервер на Mac, а затем установить сетевой прокси мобильного телефона на Mac, чтобы весь трафик на мобильном телефоне проходил через наш прокси-сервер. . Общий процесс выглядит следующим образом:
Обработка зашифрованного трафика https
В ходе фактической операции было обнаружено, что весь трафик WeChat проходил через HTTPS, в результате чего все захваченные нами зашифрованные данные не имели к нам никакого отношения. После исследования можно провести анализ HTTPS-трафика, установив корневой сертификат Charles как на мобильный телефон, так и на компьютер.Подробнее см.:
- Захват HTTPS под charles mac и захват https iphone
- Решить проблему, из-за которой Чарльз не может нормально перехватить запрос Https в iOS 11.
После установки сертификата наш трафик примерно такойПосле приведенной выше конфигурации мы уже можем читать данные запроса и ответа https, как показано на следующем рисунке.
- Та же функция может быть достигнута с помощью искателя под окнами.
- На самом деле, это очень типичный сценарий «человек посередине».
Интерфейс данных
Далее на основе этих данных узнаем нужные нам интерфейсы.После анализа в основном задействованы три интерфейса.
- Получить интерфейс информации о пользователе
- Получить интерфейс списка записей пользователя
- Получить указанный пользователем интерфейс сведений о записи
Давайте посмотрим один за другим
1. Получить интерфейс информации о пользователе
- request
API | /cgi-bin/gamewap/getpubgmbattlelist |
---|---|
метод | GET |
параметр | openid, pass_ticket, plat_id, after_time, лимит |
cookie | ключ pass_ticket, uin, pgv_pvid, sd_cookie_crttime, sd_userid |
- response
{
"user_info": {
"openid": "oODfo0pjBQkcNuR4XLTQ321xFVws",
"head_img_url": "http://wx.qlogo.cn/mmhead/Q3auHgzwzM5hSWxxxxxUQPwW9ibxxxx9DlxLTsKWk97oWpDI0rg/96",
"nick_name": "望",
"role_name": "xxxx",
"zone_area_id": 0,
"plat_id": 1
},
"battle_info": {
"total_1": 75,
"total_10": 336,
"total_game": 745,
"total_kill": 1669
},
"battle_list": [{
"map_id": 1,
"room_id": "6575389198189071197",
"team_id": 57,
"dt_event_time": 1530953799,
"rank_in_ds": 3,
"times_kill": 1,
"label": "前五",
"team_type": 1,
"award_gold": 677,
"mode": 0
}],
"appitem": {
"AppID": "wx13051697527efc45",
"IconURL": "https://mmocgame.qpic.cn/wechatgame/mEMdfrX5RU0dZFfNEdCsMJpfsof1HE0TP3cfZiboX0ZPxqh5aZnHjxPFXUGgsXmibe/0",
"Name": "绝地求生 全军出击",
"BriefName": "绝地求生 全军出击",
"Desc": "官方正版绝地求生手游",
"Brief": "枪战 | 808.2M",
"WebURL": "https://game.weixin.qq.com/cgi-bin/h5/static/detail_v2/index.html?wechat_pkgid=detail_v2&appid=wx13051697527efc45&show_bubble=0",
"DownloadInfo": {
"DownloadURL": "https://itunes.apple.com/cn/app/id1304987143",
"DownloadFlag": 5
},
"Status": 0,
"AppInfoFlag": 45,
"Label": [],
"AppStorePopUpDialogConfig": {
"Duration": 1500,
"Interval": 172800,
"ServerTimestamp": 1531066098
},
"HasEnabledChatGroup": false,
"AppType": 0,
"game_tag_list": ["绝地求生", "正版还原", "好友开黑", "百人对战", "超大地图"],
"recommend_reason": "正版绝地求生,荒野射击",
"size_desc": "808.2M"
},
"is_guest": true,
"is_blocked": false,
"errcode": 0,
"errmsg": "ok"
}
2. Получить интерфейс списка пользовательских записей
- анализировать
openid — уникальный идентификатор пользователя.
2. Получить интерфейс списка пользовательских записей
- request
API /cgi-bin/gamewap/getpubgmbattlelist метод GET параметр openid, pass_ticket, plat_id, after_time, лимит cookie ключ pass_ticket, uin, pgv_pvid, sd_cookie_crttime, sd_userid - response
{
"errcode": 0,
"errmsg": "ok",
"next_after_time": 1528120556,
"battle_list": [{
"map_id": 1,
"room_id": "6575389198111172597",
"team_id": 57,
"dt_event_time": 1530953799,
"rank_in_ds": 3,
"times_kill": 1,
"label": "前五",
"team_type": 1,
"award_gold": 677,
"mode": 0
}, {
"map_id": 1,
"room_id": "6575336498940384115",
"team_id": 11,
"dt_event_time": 1530941404,
"rank_in_ds": 5,
"times_kill": 2,
"label": "前五",
"team_type": 1,
"award_gold": 632,
"mode": 0
}],
"has_next": true
}
- анализировать
- Этот интерфейс использует after_time для пейджинга, при обходе и получении можно судить о наличии данных на следующей странице по has_next и next_after_time ответа интерфейса.
- room_id в списке — это уникальный идентификатор каждой битвы.
3. Получить интерфейс сведений о записи пользователя
- request
API | /cgi-bin/gamewap/getpubgmbattledetail |
---|---|
метод | GET |
параметр | openid, pass_ticket, room_id |
cookie | ключ pass_ticket, uin, pgv_pvid, sd_cookie_crttime, sd_userid |
- request
{
"errcode": 0,
"errmsg": "ok",
"base_info": {
"nick_name": "柚茶",
"head_img_url": "http://wx.qlogo.cn/mmhead/xxxx/96",
"dt_event_time": 1528648165,
"team_type": 4,
"rank": 1,
"player_count": 100,
"role_sex": 1,
"label": "大吉大利",
"openid": "oODfo0s1w5lWjmxxxxxgQkcCljXQ"
},
"battle_info": {
"award_gold": 622,
"times_kill": 6,
"times_head_shot": 0,
"damage": 537,
"times_assist": 3,
"survival_duration": 1629,
"times_save": 0,
"times_reborn": 0,
"vehicle_kill": 1,
"forward_distance": 10140,
"driving_distance": 5934,
"dead_poison_circle_no": 6,
"top_kill_distance": 223,
"top_kill_distance_weapon_use": 2924130819,
"be_kill_user": {
"nick_name": "小旭",
"head_img_url": "http://wx.qlogo.cn/mmhead/ibLButGMnqJNFsUtStNEV8tzlH1QpwPiaF9kxxxxx66G3ibjic6Ng2Rcg/96",
"weapon_use": 20101000001,
"openid": "oODfo0qrPLExxxxc0QKjFPnPxyI"
},
"label": "大吉大利"
},
"team_info": {
"user_list": [{
"nick_name": "ooo",
"times_kill": 6,
"assist_count": 3,
"survival_duration": 1638,
"award_gold": 632,
"head_img_url": "http://wx.qlogo.cn/mmhead/Q3auHgzwzM4k4RXdyxavNxxxxUjcX6Tl47MNNV1dZDliazRKRg",
"openid": "oODfo0xxxxf1bRAXE-q-lEezK0k"
}, {
"nick_name": "我吃炒肉",
"times_kill": 2,
"assist_count": 2,
"survival_duration": 1502,
"award_gold": 583,
"head_img_url": "http://wx.qlogo.cn/mmhead/sTJptKvBQLKd5SAAjOF0VrwiapUxxxxFffxoDUcrVjYbDf9pNENQ",
"openid": "oODfo0gIyDxxxxZpUrSrpapZSDT0"
}]
},
"is_guest": true,
"is_blocked": false
}
- анализировать
- Этот интерфейс реагирует на подробную информацию о бое, включая количество убийств, количество выстрелов в голову, количество спасений, дистанцию бега и т.д., чего нам достаточно для анализа.
- Этот интерфейс также реагирует на то, кто был убит, и на openid членов группы.Используя эту функцию, мы можем сканировать больше данных пользователей в бесконечной глубине расхождения.
Что касается такой информации, как pass_ticket в куке, то она должна использоваться для аутентификации авторизации.Эта информация не изменилась в вышеупомянутых запросах, поэтому нам не нужно глубоко изучать, как она рассчитывается, а нужно только перехватывать пакеты. чтобы извлечь информацию по умолчанию.Затем введите код, и вы можете использовать его.
Шаг 2. Сканирование данных
Интерфейс определен, и следующим шагом будет получение достаточного количества данных.
Используйте интерфейс запроса запросов для получения данных
url = 'https://game.weixin.qq.com/cgi-bin/gamewap/getpubgmdatacenterindex?openid=%s&plat_id=0&uin=&key=&pass_ticket=%s' % (openid, settings.pass_ticket)
r = requests.get(url=url, cookies=settings.def_cookies, headers=settings.def_headers, timeout=(5.0, 5.0))
tmp = r.json()
wfile = os.path.join(settings.Res_UserInfo_Dir, '%s.txt' % (rediskeys.user(openid)))
with codecs.open(wfile, 'w', 'utf-8') as wf:
wf.write(simplejson.dumps(tmp, indent=2, sort_keys=True, ensure_ascii=False))
Ссылаясь на этот метод, мы можем быстро написать два других интерфейса.
Используйте Redis, чтобы пометить просканированную информацию
В приведенном выше интерфейсе мы можем найти openid пользователя B по входу пользователя A, а затем найти openid пользователя A по входу пользователя B. Чтобы избежать повторного сбора, нам нужно записать, какая информация у нас есть. собрал. фрагмент основного кода
# rediskeys.user_battle_list 根据openid获取存在redis中的key值
def user_battle_list(openid):
return 'ubl_%s' % (openid)
# 在提取battle list之前,首先判断这用用户的数据是否已经提取过了
if settings.DataRedis.get(rediskeys.user_battle_list(openid)):
return True
# 在提取battle list之后,需要在redis中记录用户信息
settings.DataRedis.set(rediskeys.user_battle_list(openid), 1)
Используйте сельдерей для управления очередями
Celery — очень полезный инструмент для управления распределенными очередями, в этот раз я планирую запустить его только на своем компьютере, поэтому я не использую распределенные функции. Создаем три задачи и три очереди
task_queues = (
Queue('queue_get_battle_info', exchange=Exchange('priority', type='direct'), routing_key='gbi'),
Queue('queue_get_battle_list', exchange=Exchange('priority', type='direct'), routing_key='gbl'),
Queue('queue_get_user_info', exchange=Exchange('priority', type='direct'), routing_key='gui'),
)
task_routes = ([
('get_battle_info', {'queue': 'queue_get_battle_info'}),
('get_battle_list', {'queue': 'queue_get_battle_list'}),
('get_user_info', {'queue': 'queue_get_user_info'}),
],)
Затем управляйте запросами API и данными Redis в задаче, чтобы реализовать полную логику задачи, например:
@app.task(name='get_battle_list')
def get_battle_list(openid, plat_id=None, after_time=0, update_time=None):
# 判断是否已经取过用户战绩列表信息
if settings.DataRedis.get(rediskeys.user_battle_list(openid)):
return True
if not plat_id:
try:
# 提取用户信息
us = handles.get_user_info_handles(openid)
plat_id=us['plat_id']
except Exception as e:
print 'can not get user plat_id', openid, traceback.format_exc()
return False
# 提取战绩列表
battle_list = handles.get_battle_list_handle(openid, plat_id, after_time=0, update_time=None)
# 为每一场战斗创建异步获取详情任务
for room_id in battle_list:
if not settings.DataRedis.get(rediskeys.user_battle(openid, room_id)):
get_battle_info.delay(openid, plat_id, room_id)
return True
начать ползать
Поскольку мы дивергентны и искатели, нам нужно дать коду запись пользователя, поэтому нам нужно вручную создать задачу сбора данных пользователя.
from tasks.all import get_battle_list
my_openid = 'oODfo0oIErZI2xxx9xPlVyQbRPgY'
my_platid = '0'
get_battle_list.delay(my_openid, my_platid, after_time=0, update_time=None)
После того, как есть запись, используем сельдерей для запуска воркера для запуска краулера
# 启动获取用户详情worker
celery -A tasks.all worker -c 5 --queue=queue_get_user_info --loglevel=info -n get_user_info@%h
# 启动获取战绩列表worker
celery -A tasks.all worker -c 5 --queue=queue_get_battle_list --loglevel=info -n get_battle_list@%h
# 启动获取战绩详情worker
celery -A tasks.all worker -c 30 --queue=queue_get_battle_info --loglevel=info -n get_battle_info@%h
Таким образом, наш сканер может успешно работать. Затем проверьте реализацию через celery-flower.
celery flower -A tasks.all --broker=redis://:$REDIS_PASS@$REDIS_HOST:$REDIS_PORT/10
Через цветок мы видим, что эффективность работы по-прежнему очень высока.В процессе выполнения обнаружится, что get_battle_list работает слишком быстро, что приводит к отставанию get_battle_info даже при открытии 30 одновременных операций, поэтому необходимо своевременно остановить этих воркеров. Остановитесь после того, как мы получили 200 000 сообщений.
Шаг 3 Анализ данных
Программа анализа
Захвачены данные 200 000 боев, все они разбиты на json файлы и сохранены на моем локальном диске.Далее я проведу несложный анализ. Python также очень силен в области анализа данных.Есть много отличных библиотек, таких как pandas и NumPy.К сожалению, я никогда не изучал его, и для человека, который набрал только 70 баллов на вступительном экзамене в колледж по математике, анализ данных Это действительно сложно, поэтому я сам написал очень простую программу для поверхностного анализа. Крупные коровы, которым нужен глубокий анализ и которые не хотят сами ползать, могут связаться со мной, чтобы упаковать эти данные.
# coding=utf-8
import os
import json
import datetime
import math
from conf import settings
class UserTeamTypeData:
def __init__(self, team_type, player_count):
self.team_type = team_type
self.player_count = player_count
self.label = {}
self.dead_poison_circle_no = {}
self.count = 0
self.damage = 0
self.survival_duration = 0 # 生存时间
self.driving_distance = 0
self.forward_distance = 0
self.times_assist = 0 # 助攻
self.times_head_shot = 0
self.times_kill = 0
self.times_reborn = 0 # 被救次数
self.times_save = 0 # 救人次数
self.top_kill_distance = []
self.top_kill_distance_weapon_use = {}
self.vehicle_kill = 0 # 车辆杀死
self.award_gold = 0
self.times_reborn_by_role_sex = {0: 0, 1: 0} # 0 男 1 女
self.times_save_by_role_sex = {0: 0, 1: 0} # 0 男 1 女
def update_dead_poison_circle_no(self, dead_poison_circle_no):
if dead_poison_circle_no in self.dead_poison_circle_no:
self.dead_poison_circle_no[dead_poison_circle_no] += 1
else:
self.dead_poison_circle_no[dead_poison_circle_no] = 1
def update_times_reborn_and_save_by_role_sex(self, role, times_reborn, times_save):
if role not in self.times_reborn_by_role_sex:
return
self.times_reborn_by_role_sex[role] += times_reborn
self.times_save_by_role_sex[role] += times_save
def update_top_kill_distance_weapon_use(self, weaponid):
if weaponid not in self.top_kill_distance_weapon_use:
self.top_kill_distance_weapon_use[weaponid] = 1
else:
self.top_kill_distance_weapon_use[weaponid] += 1
class UserBattleData:
def __init__(self, openid):
self.openid = openid
self.team_type_res = {}
self.label = {}
self.hour_counter = {}
self.weekday_counter = {}
self.usetime = 0
self.day_record = set()
self.battle_counter = 0
def get_avg_use_time_per_day(self):
# print "get_avg_use_time_per_day:", self.openid, self.usetime, len(self.day_record), self.usetime / len(self.day_record)
return self.usetime / len(self.day_record)
def update_label(self, lable):
if lable in self.label:
self.label[lable] += 1
else:
self.label[lable] = 1
def get_team_type_data(self, team_type, player_count):
player_count = int(math.ceil(float(player_count) / 10))
team_type_key = '%d_%d' % (team_type, player_count)
if team_type_key not in self.team_type_res:
userteamtypedata = UserTeamTypeData(team_type, player_count)
self.team_type_res[team_type_key] = userteamtypedata
else:
userteamtypedata = self.team_type_res[team_type_key]
return userteamtypedata
def update_user_time_property(self, dt_event_time):
dt_event_time = datetime.datetime.fromtimestamp(dt_event_time)
hour = dt_event_time.hour
if hour in self.hour_counter:
self.hour_counter[hour] += 1
else:
self.hour_counter[hour] = 1
weekday = dt_event_time.weekday()
if weekday in self.weekday_counter:
self.weekday_counter[weekday] += 1
else:
self.weekday_counter[weekday] = 1
self.day_record.add(dt_event_time.date())
def update_battle_info_by_room(self, roomid):
# print ' load ', self.openid, roomid
file = os.path.join(settings.Res_UserBattleInfo_Dir, self.openid, '%s.txt' % roomid)
with open(file, 'r') as rf:
battledata = json.load(rf)
self.battle_counter += 1
base_info = battledata['base_info']
self.update_user_time_property(base_info['dt_event_time'])
battle_info = battledata['battle_info']
userteamtypedata = self.get_team_type_data(base_info['team_type'], base_info['player_count'])
userteamtypedata.count += 1
userteamtypedata.award_gold += battle_info['award_gold']
userteamtypedata.damage += battle_info['damage']
userteamtypedata.update_dead_poison_circle_no(battle_info['dead_poison_circle_no'])
userteamtypedata.driving_distance += battle_info['driving_distance']
userteamtypedata.forward_distance += battle_info['forward_distance']
self.update_label(battle_info['label'])
userteamtypedata.survival_duration += battle_info['survival_duration']
self.usetime += battle_info['survival_duration']/60
userteamtypedata.times_assist += battle_info['times_assist']
userteamtypedata.times_head_shot += battle_info['times_head_shot']
userteamtypedata.times_kill += battle_info['times_kill']
userteamtypedata.times_reborn += battle_info['times_reborn']
userteamtypedata.times_save += battle_info['times_save']
userteamtypedata.damage += battle_info['damage']
userteamtypedata.top_kill_distance.append(battle_info['top_kill_distance'])
userteamtypedata.update_times_reborn_and_save_by_role_sex(base_info['role_sex'], battle_info['times_reborn'],
battle_info['times_save'])
def get_user_battleinfo_rooms(self):
user_dir = os.path.join(settings.Res_UserBattleInfo_Dir, self.openid)
r = [room for room in os.listdir(user_dir)]
r = [rr.replace('.txt', '') for rr in r]
return r
class AllUserCounter:
def __init__(self):
self.hour_counter = {0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0, 10: 0, 11: 0, 12: 0, 13: 0, 14: 0, 15: 0, 16: 0, 17: 0, 18: 0, 19: 0, 20: 0, 21: 0, 22: 0, 23: 0}
self.weekday_counter = {0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0}
self.times_reborn_by_role_sex = {0: 0, 1: 0} # 0 男 1 女
self.times_save_by_role_sex = {0: 0, 1: 0} # 0 男 1 女
self.user_count = 0
self.battle_count = 0
self.every_user_use_time_per_day = []
self.top_kill_distance = 0
def avg_use_time(self):
return sum(self.every_user_use_time_per_day) / len(self.every_user_use_time_per_day)
def add_user_data(self, userbattledata):
self.every_user_use_time_per_day.append(userbattledata.get_avg_use_time_per_day())
self.battle_count += userbattledata.battle_counter
self.user_count += 1
for k in userbattledata.hour_counter:
if k in self.hour_counter:
self.hour_counter[k] += userbattledata.hour_counter[k]
else:
self.hour_counter[k] = userbattledata.hour_counter[k]
for weekday in userbattledata.weekday_counter:
if weekday in self.weekday_counter:
self.weekday_counter[weekday] += userbattledata.weekday_counter[weekday]
else:
self.weekday_counter[weekday] = userbattledata.weekday_counter[weekday]
for userteamtype in userbattledata.team_type_res:
userteamtypedata = userbattledata.team_type_res[userteamtype]
for k in userteamtypedata.times_reborn_by_role_sex:
self.times_reborn_by_role_sex[k] += userteamtypedata.times_reborn_by_role_sex[k]
for k in userteamtypedata.times_save_by_role_sex:
self.times_save_by_role_sex[k] += userteamtypedata.times_save_by_role_sex[k]
if userteamtypedata.top_kill_distance > self.top_kill_distance:
self.top_kill_distance = userteamtypedata.top_kill_distance
def __str__(self):
res = []
res.append('总用户数\t%d' % self.user_count)
res.append('总战斗数\t%d' % self.battle_count)
res.append('平均日耗时\t%d' % self.avg_use_time())
res.append('最远击杀\t%d' % max(self.top_kill_distance))
res.append('男性角色\t被救%d次\t救人%d次' % (self.times_reborn_by_role_sex[0], self.times_save_by_role_sex[0]))
res.append('女性角色\t被救%d次\t救人%d次' % (self.times_reborn_by_role_sex[1], self.times_save_by_role_sex[1]))
res.append('小时分布')
for hour in range(0, 24):
# res.append('\t%d: %d' % (hour, self.hour_counter[hour]))
res.append('\t%d: %d %.2f%%' % (hour, self.hour_counter[hour], self.hour_counter[hour]/float(self.battle_count)*100))
res.append('星期分布')
# res.append(self.weekday_counter.__str__())
for weekday in range(0, 7):
res.append('\t%d: %d %.2f%%' % (weekday+1, self.weekday_counter[weekday], (self.weekday_counter[weekday]/float(self.battle_count)*100)))
return '\n'.join(res)
def get_user_battleinfo_rooms(openid):
user_dir = os.path.join(settings.Res_UserBattleInfo_Dir, openid)
# files = os.listdir(user_dir)
r = [room for room in os.listdir(user_dir)]
r = [rr.replace('.txt', '') for rr in r]
return r
if __name__ == '__main__':
alluserconter = AllUserCounter()
folders = os.listdir(settings.Res_UserBattleInfo_Dir)
i = 0
for folder in folders:
i+=1
print i, '/' , len(folders), folder
userbattledata = UserBattleData(folder)
for room in userbattledata.get_user_battleinfo_rooms():
userbattledata.update_battle_info_by_room(room)
alluserconter.add_user_data(userbattledata)
print "\n" * 3
print "---------------------------------------"
print alluserconter
Результаты анализа
1. Среднее ежедневное время пользователя онлайн составляет 2 часа.
Судя по карте распространения, большинство пользователей тратят более 1 часа, а самые агрессивные — более 8 часов.
Примечание: здесь я считаю время выживания в каждом раунде, и фактическое время онлайн будет больше, чем у меня.
2. Женских персонажей спасают чаще, чем мужских.
Наконец-то я понял, почему так много транссексуалов, оказывается, они могут воспользоваться игрой.
3. Женские персонажи спасают больше жизней, чем мужские.
Дал всем вескую причину поднять твою сестру.
4. Пятница — самый загруженный день для всех
Предполагается, что в пятницу все будут заняты делами и написанием еженедельных отчетов.
5. 22 часа вечера — пик игры
Ранним утром так много людей играет, ты не спишь?
6. Самая длинная дистанция поражения — 639 метров.
Посмотрел эффективную дальность 98К, СКС и АРМ, а они все в пределах 800 метров, так что достоверность этого значения еще приемлемая. С другой стороны, те сверхдальние убийства Доуина должны быть инсценированы.
7. Высшая честь – получить звание «Исцеление раненых».
Из раздачи видно, что спасти мертвого сложнее, чем убить десять человек.
Большинство людей, которые могут получить звание спасателей жизней и помощи раненым, являются персонажами женского пола, что еще раз доказывает, что для игр нужно приводить сестер. Возвращаясь к сути этой игры, которая является игрой на выживание, нет ничего важнее выживания.
конец
На этот раз краулер в основном использует игровой канал WeChat для просмотра данных незнакомцев, чтобы извлечь так много данных. Мы можем анализировать данные King of Glory и других игр с помощью того же метода, и заинтересованные студенты могут попробовать его. И последнее, но не менее важное: UMP9 — хороший пистолет, и он очень крут с двукратным прицелом.