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

ClientRemoteProperty doesn't update in edge case #155

Closed
ambergamefam opened this issue Sep 15, 2023 · 9 comments
Closed

ClientRemoteProperty doesn't update in edge case #155

ambergamefam opened this issue Sep 15, 2023 · 9 comments
Labels
bug Something isn't working

Comments

@ambergamefam
Copy link
Contributor

ambergamefam commented Sep 15, 2023

Fix: #157

Currently, there is an edge case where a RemoteProperty can initialize with an old value sent from the server, and never updates with the latest value. This is actually super common in cases where Knit properties are created and updated for a player as soon as they join the game, and has caused problems in my game.

This is the problematic line of code:

return Promise.fromEvent(self._rs, function(value)

Essentially, this is the the order of things going on that makes this line of code cause problems:

  • Knit:Start() is called on the server
  • A Knit service is called which initializes a remote property
  • A player joins the game, and the property is initialized with a value for that player
  • The server immediately changes the value of this property one or more times. This queues up 3 remote events to the client that will immediately be handled once connected
  • Knit:Start() is called on the client
  • A knit controller gets the service, which instantiates the ClientRemoteProperty
  • The ClientRemoteProperty referencing the remote signal that has 3 events queued up
  • OnReady() is called, and creates a promise that automatically connects to the signal. This causes the signal to emit 3 events since this is the first time the player has connected to this remote signal. The promise only picks up the first of these 3 events, then drops the remaining 2.
  • After OnReady() completes with the first value received, the remote is connected to a second time, but this connection does not pick up the 2 queued values, dropping them entirely.

To fix this issue, the remote property either needs to initialize after the last queued-up event is processed, or it needs to queue up these changes, initialize with the first value, then emit change events for the next remote updates.

@ambergamefam ambergamefam changed the title RemoteProperties will ignore value update events from the server in edge case ClientRemoteProperty doesn't update in edge case Sep 15, 2023
@Sleitnick Sleitnick added the bug Something isn't working label Sep 16, 2023
@Sleitnick
Copy link
Owner

Sleitnick commented Sep 16, 2023

@ambergamefam Do you by chance have some simple repro code I could run to test this out? I can't seem to reproduce the original issue on my end.

I do entirely agree with the queued events logic though. If the promise is just waiting for the first event to come through, then subsequent queued events will likely get dropped. So yeah, that's definitely a problem. Not sure why I'm unable to reproduce the issue though.

@ambergamefam
Copy link
Contributor Author

ambergamefam commented Sep 18, 2023

I was able to create a repro. Looks like this only happens A) on Deferred signal behavior, and B) only in cases where data is coincidentally set for the player on the same frame where the player requests an initial value of the property from the server. In my case, since there were lots of services and properties in use, the chance of running into this issue for at least one part of the game was actually pretty high.

image

CommDeferredRepro.rbxl.zip

Source code is not rojo-managed in this case.
image


With my PR, this repro shows all values being replicated to the client, since the queued-up events are handled at once:
image

CommDeferredReproWithFix.rbxl.zip

Note that the value "5" is received twice since the initial value is sent to the player on the same frame that the value was assigned to 5.

@ambergamefam
Copy link
Contributor Author

I will be setting my game back to "Immediate" signal behavior as a workaround for the time being, but if you could take a look at my fix (#157), I would greatly appreciate it. @Sleitnick

@Sleitnick
Copy link
Owner

@ambergamefam Yup, please note the pending comments on the PR

@Sleitnick
Copy link
Owner

Fixed in Comm v0.3.2, and Knit updated to v1.5.2 with the fix

@ambergamefam
Copy link
Contributor Author

@Sleitnick There will still be an edge case if you don't create a closure when deferring the :Observe() callback
9520c61#r128355932

@Sleitnick
Copy link
Owner

What is the edge case? They should be functionally equivalent

@ambergamefam
Copy link
Contributor Author

ambergamefam commented Sep 26, 2023

From the comment I linked:

This is incorrect. This needs to be:

        task.defer(function()
                observer(self._value)
        end)

If you call task.defer with the current value, it will emit that value in a deferred thread, regardless if the value changed between now and when the observer runs. An example of this is when the value changes multiple times when multiple client events get processed in serial.

For example:

  • The client runs and asks the server the ID of their house.
  • The server receives this request and returns the ID of their house. Coincidentally (or not), a property of their house changes twice.
  • There are now 3 events queued up to the client: House ID, and then the two property changes
  • The first event processes (house ID). After the client receives the house ID, they want to begin observing these properties. They connect a callback to the remote property. This defers a thread with an old value of this property.
  • The client then processes the next two events, changing this property. The callback to :Observe() fires twice with these values
  • The deferred thread resumes, with the old value as an argument.

@ambergamefam
Copy link
Contributor Author

Essentially here's the difference:

local value = 1
task.defer(print, value)
value = 2
print(value)

Prints:

2
1

VS

local value = 1
task.defer(function()
    print(value)
end)
value = 2
print(value)

prints:

2
2

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants