Merge pull request #13 from oldnapalm/mp

This commit is contained in:
oldnapalm
2020-11-12 22:50:00 -03:00
committed by GitHub
8 changed files with 124 additions and 41 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
<head>
<title>Loading Zwift</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css" integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">
<link rel="stylesheet" href="/gameassets/site.css"/>
<link rel="stylesheet" href="/style/site.css"/>
</head>
<body>
<div class="container-fluid">
+1 -1
View File
@@ -3,7 +3,7 @@
<head>
<title>Zwift Launcher</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css" integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">
<link rel="stylesheet" href="/gameassets/site.css"/>
<link rel="stylesheet" href="/style/site.css"/>
<!--[if lte IE 9]>
<style>
.text-shadow {

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB

@@ -1,5 +1,5 @@
body {
background-image: url('https://cdn.zwift.com/gameassets/background.jpg');
background-image: url('https://cdn.zwift.com/style/background.jpg');
background-repeat: no-repeat;
background-size: cover;
overflow: hidden;
+1
View File
@@ -8,6 +8,7 @@ message World {
required uint64 world_time = 6;
required uint64 real_time = 7;
repeated Player player_states = 8;
repeated Player pace_partner_states = 12;
}
message Worlds {
+16 -8
View File
@@ -19,7 +19,7 @@ DESCRIPTOR = _descriptor.FileDescriptor(
syntax='proto2',
serialized_options=None,
create_key=_descriptor._internal_create_key,
serialized_pb=b'\n\x14protobuf/world.proto\"\x80\x01\n\x05World\x12\n\n\x02id\x18\x01 \x02(\r\x12\x0c\n\x04name\x18\x02 \x02(\t\x12\n\n\x02\x66\x33\x18\x03 \x02(\r\x12\n\n\x02\x66\x35\x18\x05 \x02(\x04\x12\x12\n\nworld_time\x18\x06 \x02(\x04\x12\x11\n\treal_time\x18\x07 \x02(\x04\x12\x1e\n\rplayer_states\x18\x08 \x03(\x0b\x32\x07.Player\" \n\x06Worlds\x12\x16\n\x06worlds\x18\x01 \x03(\x0b\x32\x06.World\"%\n\x0fWorldAttributes\x12\x12\n\nworld_time\x18\x02 \x02(\x03\"\xdb\x01\n\x06Player\x12\n\n\x02id\x18\x01 \x02(\r\x12\x11\n\tfirstName\x18\x02 \x02(\t\x12\x10\n\x08lastName\x18\x03 \x02(\t\x12\x10\n\x08\x64istance\x18\x04 \x01(\r\x12\x0c\n\x04time\x18\x05 \x01(\r\x12\n\n\x02\x66\x36\x18\x06 \x01(\r\x12\n\n\x02\x66\x38\x18\x08 \x01(\r\x12\n\n\x02\x66\x39\x18\t \x01(\r\x12\x0b\n\x03\x66\x31\x30\x18\n \x01(\r\x12\x0b\n\x03\x66\x31\x31\x18\x0b \x01(\r\x12\r\n\x05power\x18\x0c \x01(\r\x12\x0b\n\x03\x66\x31\x33\x18\r \x01(\r\x12\t\n\x01x\x18\x0e \x01(\x02\x12\x10\n\x08\x61ltitude\x18\x0f \x01(\x02\x12\t\n\x01y\x18\x10 \x01(\x02'
serialized_pb=b'\n\x14protobuf/world.proto\"\xa6\x01\n\x05World\x12\n\n\x02id\x18\x01 \x02(\r\x12\x0c\n\x04name\x18\x02 \x02(\t\x12\n\n\x02\x66\x33\x18\x03 \x02(\r\x12\n\n\x02\x66\x35\x18\x05 \x02(\x04\x12\x12\n\nworld_time\x18\x06 \x02(\x04\x12\x11\n\treal_time\x18\x07 \x02(\x04\x12\x1e\n\rplayer_states\x18\x08 \x03(\x0b\x32\x07.Player\x12$\n\x13pace_partner_states\x18\x0c \x03(\x0b\x32\x07.Player\" \n\x06Worlds\x12\x16\n\x06worlds\x18\x01 \x03(\x0b\x32\x06.World\"%\n\x0fWorldAttributes\x12\x12\n\nworld_time\x18\x02 \x02(\x03\"\xdb\x01\n\x06Player\x12\n\n\x02id\x18\x01 \x02(\r\x12\x11\n\tfirstName\x18\x02 \x02(\t\x12\x10\n\x08lastName\x18\x03 \x02(\t\x12\x10\n\x08\x64istance\x18\x04 \x01(\r\x12\x0c\n\x04time\x18\x05 \x01(\r\x12\n\n\x02\x66\x36\x18\x06 \x01(\r\x12\n\n\x02\x66\x38\x18\x08 \x01(\r\x12\n\n\x02\x66\x39\x18\t \x01(\r\x12\x0b\n\x03\x66\x31\x30\x18\n \x01(\r\x12\x0b\n\x03\x66\x31\x31\x18\x0b \x01(\r\x12\r\n\x05power\x18\x0c \x01(\r\x12\x0b\n\x03\x66\x31\x33\x18\r \x01(\r\x12\t\n\x01x\x18\x0e \x01(\x02\x12\x10\n\x08\x61ltitude\x18\x0f \x01(\x02\x12\t\n\x01y\x18\x10 \x01(\x02'
)
@@ -82,6 +82,13 @@ _WORLD = _descriptor.Descriptor(
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='pace_partner_states', full_name='World.pace_partner_states', index=7,
number=12, type=11, cpp_type=10, label=3,
has_default_value=False, default_value=[],
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
],
extensions=[
],
@@ -95,7 +102,7 @@ _WORLD = _descriptor.Descriptor(
oneofs=[
],
serialized_start=25,
serialized_end=153,
serialized_end=191,
)
@@ -126,8 +133,8 @@ _WORLDS = _descriptor.Descriptor(
extension_ranges=[],
oneofs=[
],
serialized_start=155,
serialized_end=187,
serialized_start=193,
serialized_end=225,
)
@@ -158,8 +165,8 @@ _WORLDATTRIBUTES = _descriptor.Descriptor(
extension_ranges=[],
oneofs=[
],
serialized_start=189,
serialized_end=226,
serialized_start=227,
serialized_end=264,
)
@@ -288,11 +295,12 @@ _PLAYER = _descriptor.Descriptor(
extension_ranges=[],
oneofs=[
],
serialized_start=229,
serialized_end=448,
serialized_start=267,
serialized_end=486,
)
_WORLD.fields_by_name['player_states'].message_type = _PLAYER
_WORLD.fields_by_name['pace_partner_states'].message_type = _PLAYER
_WORLDS.fields_by_name['worlds'].message_type = _WORLD
DESCRIPTOR.message_types_by_name['World'] = _WORLD
DESCRIPTOR.message_types_by_name['Worlds'] = _WORLDS
+51 -7
View File
@@ -44,6 +44,7 @@ globalGhosts = {}
ghostsEnabled = {}
online = {}
playerUpdateQueue = {}
globalPacePartners = {}
def roadID(state):
return (state.f20 & 0xff00) >> 8
@@ -195,13 +196,10 @@ class CDNHandler(SimpleHTTPRequestHandler):
# This can only work if we're running on a different machine than the Zwift client.
sent = False
try:
url = 'http://{}{}'.format(hostname, self.path)
req_header = self.parse_headers()
resp = requests.get(url, headers=merge_two_dicts(req_header, set_header()), verify=False)
sent = True
self.send_response(resp.status_code)
self.send_resp_headers(resp)
self.wfile.write(resp.content)
@@ -345,6 +343,33 @@ class GhostsVariables:
start_road = 0
start_rt = 0
class PacePartnerVariables:
route = None
position = 0
def loadPacePartners():
folder = '%s/pace_partners' % STORAGE_DIR
if not os.path.isdir(folder): return
for (root, dirs, files) in os.walk(folder):
for pp_id in dirs:
route = '%s/%s/route.bin' % (folder, pp_id)
if os.path.isfile(route):
with open(route, 'rb') as fd:
globalPacePartners[pp_id] = PacePartnerVariables()
pp = globalPacePartners[pp_id]
pp.route = udp_node_msgs_pb2.Ghost()
pp.route.ParseFromString(fd.read())
pp.position = 0
def playPacePartners():
while True:
keys = globalPacePartners.keys()
for pp_id in keys:
pp = globalPacePartners[pp_id]
if pp.position < len(pp.route.states) - 1: pp.position += 1
else: pp.position = 0
ppthreadevent.wait(timeout=3)
class UDPHandler(socketserver.BaseRequestHandler):
def handle(self):
data = self.request[0]
@@ -383,13 +408,13 @@ class UDPHandler(socketserver.BaseRequestHandler):
ghosts.lastPackageTime = t
if player_id in ghostsEnabled and ghostsEnabled[player_id]:
if not ghosts.loaded and state.roadTime > 0:
if not ghosts.loaded and getCourse(state):
ghosts.loaded = True
loadGhosts(player_id, state, ghosts)
if state.roadTime and ghosts.last_rt and state.roadTime != ghosts.last_rt:
if t >= ghosts.last_rec + update_freq:
state = ghosts.rec.states.add()
state.CopyFrom(state)
s = ghosts.rec.states.add()
s.CopyFrom(state)
ghosts.last_rec = t
if not ghosts.started and ghosts.play.ghosts and roadID(state) == ghosts.start_road:
if isForward(state):
@@ -473,12 +498,26 @@ class UDPHandler(socketserver.BaseRequestHandler):
#Check if players are close in world
if zwift_offline.isNearby(state, player):
nearby.append(p_id)
for p_id in globalPacePartners.keys():
pace_partner_variables = globalPacePartners[p_id]
pace_partner = pace_partner_variables.route.states[pace_partner_variables.position]
#Check if pacepartner is close to player in world
if zwift_offline.isNearby(state, pace_partner):
nearby.append(p_id)
players = len(nearby)
message.num_msgs = players // 10
if players % 10: message.num_msgs += 1
for p_id in nearby:
player = None
if p_id in online.keys():
player = online[p_id]
elif p_id in globalPacePartners.keys():
pace_partner_variables = globalPacePartners[p_id]
player = pace_partner_variables.route.states[pace_partner_variables.position]
player.id = int(p_id)
player.watchingRiderId = player.id
player.worldTime = zwift_offline.world_time()
if player != None:
if len(message.states) < 10:
state = message.states.add()
state.CopyFrom(player)
@@ -513,4 +552,9 @@ udpserver_thread = threading.Thread(target=udpserver.serve_forever)
udpserver_thread.daemon = True
udpserver_thread.start()
zwift_offline.run_standalone(online, ghostsEnabled, saveGhost, playerUpdateQueue)
loadPacePartners()
ppthreadevent = threading.Event()
pp = threading.Thread(target=playPacePartners)
pp.start()
zwift_offline.run_standalone(online, globalPacePartners, ghostsEnabled, saveGhost, playerUpdateQueue)
+53 -23
View File
@@ -20,7 +20,7 @@ from io import BytesIO
from shutil import copyfile
import jwt
from flask import Flask, request, jsonify, g, redirect, render_template, url_for, flash, session, abort, make_response, send_file
from flask import Flask, request, jsonify, g, redirect, render_template, url_for, flash, session, abort, make_response, send_file, send_from_directory
from flask_login import UserMixin, AnonymousUserMixin, LoginManager, login_user, current_user, login_required
from google.protobuf.descriptor import FieldDescriptor
from protobuf_to_dict import protobuf_to_dict, TYPE_CALLABLE_MAP
@@ -42,6 +42,7 @@ import protobuf.hash_seeds_pb2 as hash_seeds_pb2
logging.basicConfig(filename='zoffline.log', level=os.environ.get("LOGLEVEL", "INFO"))
logger = logging.getLogger('zoffline')
logger.setLevel(logging.WARN)
logging.getLogger('sqlalchemy.engine').setLevel(logging.WARN)
if os.name == 'nt' and platform.release() == '10' and platform.version() >= '10.0.14393':
# Fix ANSI color in Windows 10 version 10.0.14393 (Windows Anniversary Update)
@@ -92,6 +93,7 @@ app.config['MAX_CONTENT_LENGTH'] = 1024 * 1024
db = SQLAlchemy(app)
online = {}
globalPacePartners = {}
ghostsEnabled = {}
playerUpdateQueue = {}
playerIds = {}
@@ -554,6 +556,12 @@ def api_zfiles():
return zfile.SerializeToString(), 200
# Custom static data
@app.route('/style/<path:filename>')
def custom_style(filename):
return send_from_directory('%s/cdn/style' % SCRIPT_DIR, filename)
# Probably don't need, haven't investigated
@app.route('/api/zfiles/list', methods=['GET', 'POST'])
def api_zfiles_list():
@@ -807,7 +815,7 @@ def api_profiles_activities_id(player_id, activity_id):
player_id = current_user.player_id
if current_user.enable_ghosts:
try:
saveGhost(activity.name, int(player_id))
saveGhost(activity.name, player_id)
except:
pass
# Unconditionally *try* and upload to strava and garmin since profile may
@@ -852,14 +860,9 @@ def api_profiles_followees(player_id):
def get_week_range(dt):
d = datetime.datetime(dt.year,1,1)
if (d.weekday()<= 3):
d = d - timedelta(d.weekday())
else:
d = d + timedelta(7-d.weekday())
dlt = timedelta(days = (int(dt.strftime('%W'))-1)*7)
first = d + dlt
last = d + dlt + timedelta(days=6, hours=23, minutes=59, seconds=59)
d = datetime.datetime(dt.year,dt.month,dt.day - dt.weekday())
first = d
last = d + timedelta(days=6, hours=23, minutes=59, seconds=59)
return first, last
def get_month_range(dt):
@@ -1075,6 +1078,31 @@ def relay_worlds_generic(world_id=None):
onlinePlayer.altitude = player.altitude
onlinePlayer.y = player.y
playersInRegion += 1
for p_id in globalPacePartners.keys():
pace_partner_variables = globalPacePartners[p_id]
pace_partner_id = int(p_id)
pace_partner = pace_partner_variables.route.states[pace_partner_variables.position]
courseId = getCourse(pace_partner)
if course == courseId:
partialProfile = getPartialProfile(pace_partner_id)
if not partialProfile == None:
online_pace_partner = world.pace_partner_states.add()
online_pace_partner.id = pace_partner_id
online_pace_partner.firstName = partialProfile.first_name
online_pace_partner.lastName = partialProfile.last_name
online_pace_partner.distance = pace_partner.distance
online_pace_partner.time = pace_partner.time
online_pace_partner.f6 = 840#0
online_pace_partner.f8 = 0
online_pace_partner.f9 = 0
online_pace_partner.f10 = 0
online_pace_partner.f11 = 0
online_pace_partner.power = pace_partner.power
online_pace_partner.f13 = 2355
online_pace_partner.x = pace_partner.x
online_pace_partner.altitude = pace_partner.altitude
online_pace_partner.y = pace_partner.y
playersInRegion += 1
world.f5 = playersInRegion
if world_id:
world.id = world_id
@@ -1101,10 +1129,12 @@ def relay_worlds_id_join(world_id):
@app.route('/relay/worlds/<int:world_id>/players/<int:player_id>', methods=['GET'])
def relay_worlds_id_players_id(world_id, player_id):
for p_id in online.keys():
player = online[p_id]
if player.id == player_id:
return player.SerializeToString()
if player_id in online.keys():
player = online[player_id]
return player.SerializeToString()
if str(player_id) in globalPacePartners.keys():
pace_partner = globalPacePartners[str(player_id)]
return pace_partner.route.states[pace_partner.position].SerializeToString()
return None
@@ -1242,6 +1272,9 @@ def relay_worlds_leave(world_id):
@app.teardown_request
def teardown_request(exception):
db.close_all_sessions()
db.session.close()
db.engine.dispose()
if exception != None:
print('Exception: %s' % exception)
@@ -1284,7 +1317,6 @@ def init_database():
def check_columns():
time.sleep(3)
rows = db.engine.execute(sqlalchemy.text("PRAGMA table_info(user)"))
should_have_columns = User.metadata.tables['user'].columns
current_columns = list()
@@ -1315,8 +1347,7 @@ def before_first_request():
init_database()
db.create_all(app=app)
db.session.commit()
check_columns_thread = threading.Thread(target=check_columns)
check_columns_thread.start()
check_columns()
send_message_thread = threading.Thread(target=send_server_back_online_message)
send_message_thread.start()
@@ -1439,18 +1470,15 @@ def auth_realms_zwift_tokens_access_codes():
return FAKE_JWT, 200
@app.route('/static/web/launcher/<filename>', methods=['GET'])
def static_web_launcher(filename):
return render_template(filename)
def run_standalone(passedOnline, passedGhostsEnabled, passedSaveGhost, passedPlayerUpdateQueue):
def run_standalone(passedOnline, passedGlobalPacePartners, passedGhostsEnabled, passedSaveGhost, passedPlayerUpdateQueue):
global online
global globalPacePartners
global ghostsEnabled
global saveGhost
global playerUpdateQueue
global login_manager
online = passedOnline
globalPacePartners = passedGlobalPacePartners
ghostsEnabled = passedGhostsEnabled
saveGhost = passedSaveGhost
playerUpdateQueue = passedPlayerUpdateQueue
@@ -1460,6 +1488,8 @@ def run_standalone(passedOnline, passedGhostsEnabled, passedSaveGhost, passedPla
if not MULTIPLAYER:
login_manager.anonymous_user = AnonUser
login_manager.init_app(app)
db.close_all_sessions()
db.engine.dispose()
@login_manager.user_loader
def load_user(uid):