Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ghosts feature #56

Closed
oldnapalm opened this issue May 18, 2020 · 63 comments
Closed

Ghosts feature #56

oldnapalm opened this issue May 18, 2020 · 63 comments

Comments

@oldnapalm
Copy link
Collaborator

oldnapalm commented May 18, 2020

I'm trying to implement ghosts feature in zoffline, it was requested by @scouseman in #35 (comment)

PlayerState structure is defined in https://github.com/Ogadai/zwift-mobile-api/blob/master/src/zwiftMessages.proto

The main issue I found (for now) is that rider position is not determined by x, y, altitude and heading, the only fields that seem to influence are roadTime and speed.

Found in https://github.com/wiedmann/zwift-line-monitor that it's an integer value from 5000 to 1005000, verified it by reading values sent from client to zoffline, but couldn't find a relation between roadTime and distance.

Values from a 5 km ride in Fuego Flats
https://github.com/oldnapalm/zwift-offline/blob/ghost/flats.csv

This is what I've tried so far
https://github.com/oldnapalm/zwift-offline/blob/ghost/udp.py

Any help or ideas are welcome.

Edit: this test is with a roadTime increment of 38 per meter. Around km 0.3 the ghost does some strange maneuvers, looks like it's when roadTime value becomes too off. It happens again around km 1.3

https://youtu.be/tdMKQu5L0tQ

Maybe the only way is storing roadTime values (one per meter?) for each segment?

Edit: maybe a better approach would be, instead of using FIT or GPX files for ghosts, making the UDP server store one full PlayerState record each 5 seconds during activities and save a file in player_id/ghosts/world_id/road_id directory. When the player gets on that road again (in the same direction) the server sends back the ghost. It would work only for new activities, but easier to implement.

Edit: here is my first try on the second approach
https://github.com/oldnapalm/zwift-offline/blob/ghost/udp2.py

Problems:

  • spawn location seems to be random
  • some routes leave a road and go back to it at a different location
  • need MAP_OVERRIDE to know in which world we are

Maybe it's a better idea to store the ghost for the entire activity, not per road.

Edit: this is the 3rd approach, my favorite so far. It saves ghosts in storage/player_id/ghosts and loads from storage/player_id/ghosts/load
https://github.com/oldnapalm/zwift-offline/blob/ghost/udp3.py

@oldnapalm
Copy link
Collaborator Author

oldnapalm commented May 20, 2020

Merged the changes (3rd approach) into standalone.py in https://github.com/oldnapalm/zwift-offline/tree/ghost

How to use:

  • Create a file called "enable_ghosts.txt" inside storage folder
  • When rider stops, a ghost file will be saved in storage/player_id/ghosts
  • Copy a ghost file to storage/player_id and rename it to ghost.bin ghost files to storage/player_id/ghosts/load, on next ride they will be loaded (you can rename the files, e.g. Sands and Sequoias PR.bin)
  • You must ensure that the ghost files are from the same map and route you are going to ride

Issues:

  • random spawn position (although in Watopia usually not very far). In London this is a bigger problem, didn't test other maps yet added a delay as a possible workaround, ghost will start when you reach its spawn location
  • must do a full stop to save the ghost (no big deal, just can't close Zwift with rider moving) save ghost on Zwift exit
  • rely on TCP connection to clear ghost data (in case you restart Zwift without restarting zoffline) use a timeout instead

If someone wants to test this I can create a Windows release.

@RobinGhost
Copy link

RobinGhost commented May 20, 2020 via email

@oldnapalm
Copy link
Collaborator Author

oldnapalm commented May 20, 2020

Here it goes

Edit: updated release, put ghost files in storage/player_id/ghosts/load

zoffline_1.0.49821_ghosts_test5.zip

In this test version you don't need to create the file enable_ghosts.txt

Remember to backup your files, mainly if you are updating from previous Zwift version, the database will be updated to fix segment timing issue.

https://youtu.be/4LAYMvOSGPU

@RobinGhost
Copy link

RobinGhost commented May 21, 2020 via email

@oldnapalm
Copy link
Collaborator Author

Before testing please check my previous comment again, maybe I will update the release before the weekend.

@RobinGhost
Copy link

RobinGhost commented May 23, 2020 via email

@oldnapalm
Copy link
Collaborator Author

Hi, thanks for the feedback.

Yes, that's expected because it's the way I found to know when to save the ghost (save when the rider stops). Need to find a better way to determine when to save the ghost. For now ghosts must be always moving.

I found another issue, if you change your weight, the W/Kg info for the ghost will be wrong, but it's just cosmetic, won't affect the ghost "performance".

Except that it's working fine for me.

https://youtu.be/b94Xwn387TM

@RobinGhost
Copy link

RobinGhost commented May 23, 2020 via email

@oldnapalm
Copy link
Collaborator Author

Are you using the first release? I updated to make the ghost start when you reach it's spawn location. Another option is to give the ghost a few seconds to start moving, so you don't get too ahead.

About the save issue, probably going to make it save the ghost when the activity is saved (client calls /api/profiles/<int:player_id>/activities/<string:activity_id> with upload-to-strava argument.

Observed another issue where the ghost disappears and reappears. Happened arriving a hairpin turn, I'm guessing it's because the 3 seconds update frequency.

@RobinGhost
Copy link

RobinGhost commented May 23, 2020 via email

@oldnapalm
Copy link
Collaborator Author

oldnapalm commented May 23, 2020

The client only calls /api/profiles/<int:player_id>/activities/<string:activity_id> with upload-to-strava argument after 5 km, so for now we will save ghosts on /api/users/logout. It will save when you exit Zwift, no matter if you click "save activity" or the trash bin.

This is a new test version
zoffline_1.0.49821_ghosts_test6.zip

Changes:

  • save ghost on Zwift exit
  • ghost will start when you pass its spawn position plus 15 seconds
  • if multiple ghosts, they will start when you pass the one in the back
  • if you spawn ahead of the ghost, it will start once you start pedaling

Other options I thought about:

  • ghost starts when you pass its spawn position (you can have an advantage if you pass it fast and the ghost takes time to gain speed)
  • with multiple ghosts, they start when you pass the one in the front

The disappear/reappear issue was in a test with 4 ghosts, it may also be related to draft between ghosts. If the ghost drafts and gets ahead of its recorded roadTime, at some point it will be too off and need to be relocated. Edit: it's probably related to drafting, removed 2 other ghosts and the problematic one started to behave normally.

Another question: when you stop during an activity, do you want the stopped time to be recorded or not? Right now only moving time is being saved to ghost file.

@RobinGhost
Copy link

RobinGhost commented May 23, 2020 via email

@oldnapalm
Copy link
Collaborator Author

Ok, thanks for the testing and feedbacks.

I have 2 questions:

  • when you stop during an activity, do you want the stopped time to be recorded or not? Are these stops expected? Right now only moving time is being saved to ghost file.
  • if you are riding with a ghost and you stop, do you want the ghost to stop too, or continue moving?

@RobinGhost
Copy link

RobinGhost commented May 24, 2020 via email

@oldnapalm
Copy link
Collaborator Author

I don't know, because I don't race, my workouts are usually under 1 hour and I rarely stop.

My wife said the ghosts should stop when you stop (she also doesn't use to stop, only in "emergencies"). Maybe it can be an option. She also thinks the stopped time should be disregarded when recording ghosts.

What do you think about recording the stopped time?

@RobinGhost
Copy link

RobinGhost commented May 24, 2020 via email

@oldnapalm
Copy link
Collaborator Author

I agree with you, to simulate a race the ghost can't stop if you stop.

So I'm keeping the test6 behavior, stop time is disregarded when saving ghost, and ghost don't stop when you stop.

@RobinGhost
Copy link

RobinGhost commented May 24, 2020 via email

@oldnapalm
Copy link
Collaborator Author

This is a new test release with minor changes:

  • Ghost will be saved on activity save (must have at least 5 km, like upload to Strava).
  • Removed the 15 seconds in ghost delay. If you spawn too close to the ghost it will have an advantage because it starts at full speed. If you spawn behind you can go slow until the ghost spawns to have a fair dispute (like if you spawn ahead, you can wait for the ghost to get closer).
  • Fixed a delay bug in roads that start in reverse direction (decreasing roadTime).

zoffline_1.0.49821_ghosts_test7.zip

Added the changes in #57

Please let me know if you find any issues. Thanks.

@RobinGhost
Copy link

RobinGhost commented May 24, 2020 via email

@oldnapalm
Copy link
Collaborator Author

Glad you enjoyed it.

Added a release here
https://github.com/oldnapalm/zwift-offline/releases/tag/Zwift_1.0.49821_r2

Cheers

@oldnapalm
Copy link
Collaborator Author

This is the bug I mentioned before
https://youtu.be/ABB4ayd8p9g

It happens when there are various ghosts. My guess is Zwift calculates draft between nearby players (ghosts in this case), at some point roadTime is too ahead of value received from server, the rider makes a loop, roadTime gets behind the value from server, then the rider disappears and reappears a little ahead, or takes a shortcut to reach roadTime from server.

@RobinGhost
Copy link

RobinGhost commented May 27, 2020 via email

@oldnapalm
Copy link
Collaborator Author

The ghost feature has been merged into zoffline.

I'm closing this issue. Feel free to reopen it if you find anything else.

Cheers

oldnapalm added a commit that referenced this issue Jun 5, 2020
Caused the bug mentioned here
#56 (comment)
@oldnapalm
Copy link
Collaborator Author

The supposed drafting bug actually happened because of this misplaced increment 62a4161

When a ghost finished, others with higher id would get their id decremented.

@msobecki
Copy link

Biggest difference in roadTime between two states near start line at Volcano Circuit was ~7700 (difference before last point before start and first point after), so 4000 should do the work.
On the other hand, maybe instead of finding nearest state to start line just take the first state after passing start line and deal with it, that ghosts will spawn a little bit after start line. States are saved every 3 seconds, so there always will be a little bit of inaccuracy.

Anyway, it gives a lot of fun to ride with ghosts so thanks again for you work :)

I just saw edit in your previous comment, will try to look at this commit and run this version tomorrow.

@RobinGhost
Copy link

RobinGhost commented Jun 15, 2020 via email

@oldnapalm
Copy link
Collaborator Author

oldnapalm commented Jun 15, 2020

On the other hand, maybe instead of finding nearest state to start line just take the first state after passing start line and deal with it, that ghosts will spawn a little bit after start line.

It's a good idea but I couldn't think of a way to implement that. Would have to look for 2 adjacent points where one is > and other is < than start line. Also need to know when we pass the start line, and there's the roadTime "reset" situation (from 1005000 to 5000 and vice versa). Will think about it.

Test version with commit oldnapalm@6a8e2c2

zoffline_1.0.51959_test4.zip

@oldnapalm
Copy link
Collaborator Author

Sorry still not had a chance to do any testing my return home has been delayed. As soon as I get back I will get right onto it

Don't worry, we are all doing this for fun ;)

@oldnapalm
Copy link
Collaborator Author

oldnapalm commented Jun 16, 2020

@msobecki think this should work, check when pass the start line instead of when get close oldnapalm@77cef21

zoffline_1.0.51959_test5.zip

Need to test if will work in all cases.

If you want the ghosts to spawn further back, just remove this
https://github.com/oldnapalm/zwift-offline/blob/77cef21a27aac45547a5fff708c449d7bbf89817/standalone.py#L113

@msobecki
Copy link

Great job @oldnapalm, I'll try to check it tomorrow, looks promising.
BTW. I can't check it now, but how often does zwift client send updates with player state to the server? Now state is saved every 3 seconds, does it make sense to change update_freq to, for example 2 seconds? or every second? It will make bin files bigger, but does it have any other negative consequences? any performance issues?

thanks

@oldnapalm
Copy link
Collaborator Author

oldnapalm commented Jun 16, 2020

Don't know exactly, but it's more than once a second. You can change update_freq to 2 or 1.

https://github.com/oldnapalm/zwift-offline/blob/64888a75830f91aaf9af98cad54f8219c0726a77/standalone.py#L47

I believe there are no other negative consequences, just the bin files bigger. Files saved with different update_freq won't work.

If you will run from source, please use this branch, I made a few changes since the last blob https://github.com/oldnapalm/zwift-offline/blob/start-line/standalone.py

Another issue with the start line is when there are 2 or more routes with the same spawn point and different start lines (like jungle circuit and road to sky) you can't have both in the csv because we don't know which route you are going to ride when you spawn.

@oldnapalm
Copy link
Collaborator Author

Once, with 4 ghosts, I couldn't find them after spawning, but after restarting Zwift next time they spawned just fine.

@msobecki if that happens again please check Zwift log for UDP errors or timeout

NETCLIENT:[ERROR] Error receiving UDP datagram [234] Existem mais dados disponíveis.

Not sure if it's the cause but I think we should disregard incoming packets if can't decode as ClientToServer

except:

@msobecki
Copy link

Thanks @oldnapalm
Tried to test something yesterday but I had some issues with my environment. Compiled version test5 worked fine (running on the same machine as zwift). I tried latest version from your start-line branch but when I run it as standalone from sources on another computer, I couldn't even start zwift (problem with initial connection to the server), don't know why but it's for sure something in my environment, have to check it once again. Ended up with running docker image with mounted directory with latest sources as volume in image (+ another volume for storage) on another computer and I was able to connect, but no ghost spawned. Today I realized, that I didn't expose additional ports (3022, 3023), so I guess it explains why. I'll try to check my env in next days to be able to run it from sources.

Thanks again, BR.

@oldnapalm
Copy link
Collaborator Author

oldnapalm commented Jun 18, 2020

when I run it as standalone from sources on another computer, I couldn't even start zwift (problem with initial connection to the server)

Maybe you didn't allow python.exe in Windows firewall?

Today I realized, that I didn't expose additional ports (3022, 3023), so I guess it explains why.

Just adding the ports in this line solves the problem or need something else? Thanks for that.

EXPOSE 443 80

Here's a bundle with the latest changes

zoffline_1.0.51959_test6.zip

And the updated csv (road to sky doesn't work, need to figure a way to fix)

https://github.com/oldnapalm/zwift-offline/blob/start-line/start_lines.csv

Added a few more start lines (untested). Removed jungle and desert (breaks ghost spawn for alpe and titans grove).

@oldnapalm
Copy link
Collaborator Author

Another update, need to store the spawn direction in the csv because there are routes in both directions. Fixes desert and titans grove. Road to sky is still broken if jungle circuit start line is present. Other maps untested, just added the start lines, but should work.

https://github.com/oldnapalm/zwift-offline/blob/start-line/start_lines.csv

zoffline_1.0.51959_test7.zip

@oldnapalm
Copy link
Collaborator Author

Update

zoffline_1.0.51959_test8.zip

Use spawn direction only when start line is different for each direction

https://github.com/oldnapalm/zwift-offline/blob/start-line/start_lines.csv

@defiancecp
Copy link

I think I found a bug! But on the other hand I'm just scratching the surface with this code so sharing thoughts here to see if I'm completely off base :)

Context: I've been working through an attempt to set up so that ghosts pick up at the start of the Alpe du Zwift climb, which has been tricky. Finally got it to work, but one of the things that gave trouble turned out to be the 'isForward' comparison when intaking the csv. As it's written, it's comparing the text strings of the isForward status, with the isForward indicator in the csv. Problem is, one is capitalized, other is not (based on the capitalization in existing rows of the csv) - so it was handling most fine because they were blank (so falling into the 'or not' condition), but those that were populated were trying to compare 'TRUE' to 'True' (for example), and thus thinking there was no matching starting line defined.

One super simple fix would be to just match cases in the csv file, but really that seems like leaving a vector for future errors if someone enters true/false in the wrong case at some future point. It seems like a better way would be to make a quick/simple boolean check like:

def booleans(v):
return v.lower() in ("yes", "true", "t", "1")

And then in the csv row check, update it to:

        rt = [t for t in sl if t[0] == str(course(state)) and t[1] == str(roadID(state)) and (booleans(t[2]) == booleans(str(isForward(state))) or not t[2])]

Basically just forces case and comparing to a list of strings that would evaluate to true, otherwise evaluate to false, creating a super-quick/rough string to boolean converter.

With the boolean/case check fixed, all worked :) To get road to sky + Alpe du Zwift working, I removed:

6,36,,35,546940,Jungle Circuit
And added:
6,36,True,43,22997,Road to Sky

With this result (success!):
https://youtu.be/5OjqQ7PGLIk

As I'm looking through this, I'm wondering if it wouldn't be pretty straightforward to do more of a segment-based ghost system... Going to do some experimentation, if I come up with any ideas solid enough to share I'll do so here.

@oldnapalm
Copy link
Collaborator Author

Yes, the current code expects the csv to contain "True" or "False" (case sensitive).

Your booleans function could be

def booleans(v):
    if v[0].lower() in ['y', 't', '1']:
        return True
    return False

then the check could be booleans(t[2]) == isForward(state)

But in this specific case you don't need to check because the routes starting on this road are in the same direction, so you can leave it blank (falls in not t[2]).

Only Desert Flats/Titans Grove require this check because there are routes starting on the same road, going in both directions, with different starting points (if the start line is the same for both directions, like Downtown and UCI courses, it's also not necessary).

I think it would be simple to do a segment based ghost system. I tried a road based system in my early tests, but then I found that saving a ghost for the entire activity would be more useful to me. Tomorrow I can send you the code if you want to take a look.

@defiancecp
Copy link

Of course I'd love to take a look 😄

Aha! I see why I called it a bug: user error of course :)
-- When I opened the .csv for the first time, it opened in excel - And excel ... er... autocorrected 'True' to 'TRUE'

@oldnapalm
Copy link
Collaborator Author

As you can see the csv reading is very basic, things can go wrong is the file is not perfectly formatted (e.g. start_rt = int(rt[0][4]) can cause a ValueError if there's a letter there).

I'm attaching the code I mentioned yesterday, but looking at it now I don't think it will help with your idea, it's very preliminar (e.g. used MAP_OVERRIDE instead of course for the first directory level). It's the second approach mentioned in the opening of this issue.

udp2.py.zip

oldnapalm added a commit that referenced this issue Aug 17, 2020
Looks like the issue mentioned in #56 (comment) is actually caused by a second UDP connection before the ghosts are loaded
@oldnapalm
Copy link
Collaborator Author

oldnapalm commented Aug 20, 2020

Hello @scouseman @msobecki @defiancecp how are you?

Any of you are still using this or have many ghosts for a route?

I had an issue when I reached 13 ghosts for the same route, looks like it exceeds the UDP datagram size limit.

NETCLIENT:[ERROR] Error receiving UDP datagram [234] Existem mais dados disponíveis.

I tried to fix it by sending multiple messages with a maximum of 10 ghosts per message. It seems to be working for this specific case, but I would like to do some more tests, please let me know if you can help.

Thanks.

@oldnapalm oldnapalm reopened this Aug 20, 2020
@msobecki
Copy link

Hi @oldnapalm
Unfotunately, both my trainer and my laptop died some time ago, so I won't be able to help with testing right now.

But of course, again, thanks for your work and commitment 👍

oldnapalm added a commit that referenced this issue Aug 24, 2020
Looks like Excel converts True/False to uppercase.
Thanks @defiancecp #56 (comment)
@oldnapalm
Copy link
Collaborator Author

oldnapalm commented Sep 13, 2020

After a few months of use and some bugs fixed, I changed the ghosts handling a bit, now it saves the files organized in <course>/<road> subdirectories (previously existing files will be organized) and ghosts will be loaded automatically (no need to copy to load directory, it was getting boring to manage the files manually).

Also added a checkbox in the launcher window to easily enable/disable the feature.

@oldnapalm oldnapalm changed the title Add ghosts feature Ghosts feature Dec 19, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants