RunService:IsClient() and RunService:IsServer() do not work in Play Test / Play Solo

Both these methods seem to return “true” 100% of the time.
This is a bug that’s been around a long time due to Play Solo acting as both the client and the server simultaneously.

I think it’s more intuitive for RunService:IsClient() to return true only when called from a localscript or from a modulescript which was required by a localscript.

And of course, vice versa for RunService:IsServer().

Currently, it’s practically impossible to have a module that is used by both server and client and be able to differentiate between the two in Play Solo.

8 Likes

You’re not wrong and I agree with you. But I haven’t actually come across an example where this has made testing a game impossible - could you offer an example? Not saying it wouldn’t happen, I’m actually just curious and it would also help bolster your argument.

1 Like

My use case here is a little hacky, but I don’t think every use case for something like this would be hacky.

I’m creating a network wrapper class and need to know if it’s being used from the server or client to choose which methods to run from RemoteEvent/RemoteFunction.

1 Like

Ahh gotcha. At least for that example, I guess you COULD technically just add an IsStudio thing that uses Bindable events but that does seem fairly ridiculous just for such a simple thing.

This behavior was intentional when adding this API, we can probably change it though. If we change it we should also add RunService:IsPlaySolo() because currently checking that both IsServer and IsClient are true is the only way to check if the game is being run in solo mode.

7 Likes

I mean, isn’t :IsStudio() enough?

That would return true with Start Player / Start Server.

1 Like

Oh, true, that situation escaped my mind.

I don’t think this change would make sense. In play solo, you are the client and server in the same Studio instance. Modules are also loaded only once when required from a LocalScript or normal Script, instead of having multiple instances of the module active if you require it for clients and the server (once per client + one for the server) (so in play solo the module only runs once and the return value is shared across all require calls).

Especially in the above case it is difficult to properly define if the module is running on the client or server (as they all share the same module instance). Does it depend on whether a LocalScript or normal Script required it first? Currently, because in Play solo everything is basically regarded the client and server, this concurrency issue does not really matter, which I regard as a good thing.

Best is obviously emulating a client + server separation play solo, so that the module is actually executed twice (but the advantage of Play solo is mostly that it’s super fast in my opinion).

7 Likes

Maybe script:IsLocal() would be a better option, since it’s clarifying that the script is local and not the Studio instance? For modules, would return the result of the requiring script.

IsLocal and IsClient are so similar and, aside from PlaySolo, would return the exact same thing. That would just be API clutter. I think either of the two ideas given by @Osyris and @Den_S are much more optimal and make a lot more sense.

The only time when you can associate a require with the requiring script is when you initially require it because ModuleScripts only run once and store the returned value forever for that play session. But you shouldn’t do that as it’s not always defined which script requires it first. It sounds very weird to change behavior depending on what (kind of) script required it first anyway, in my opinion.

Note that after the initial execution, the return value has no (direct) association with the module anymore either, it is just a plain Lua object like any other (the only difference is that the environment of functions defined inside of the modulescript is different, which has no relation to whatever required the script initially or what script is currently calling that function).

However, you can get the environment of (for example) the calling function and check if the script variable there is a LocalScript.

For example, putting this in a ModuleScript, then requiring + running it from a LocalScript and normal Script will behave differently depending on the environment of the calling function.

return function()
	local execFromLocal = getfenv(2).script:IsA('LocalScript');
	print('is called from a localscript?', execFromLocal);
end;

Just don’t do that please ;).

I actually ran into this problem a few days ago:

Use case: I have a module called “Networking”, and both client and server load it, there is an initiate function, but only the server goes past a certain spot to create objects, etc to be replicated. I never thought of :isPlaySolo() or IsStudio(), instead i just checked if the objects folder existed instead.

This seems like intended behavior to me

To solve this issue you should make it so your modules do not hold state. There are two good reasons for this:

  • Your modules are reusable
  • Your modules can be used on both server and client codebases without issue

You can use dependency injection and instantiate new stateful objects on both the client and server to handle this.

2 Likes