mirror of
https://github.com/zoffline/zwift-offline.git
synced 2026-08-03 01:15:46 -07:00
Merge pull request #13 from oldnapalm/mp
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
@@ -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
@@ -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
@@ -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
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user