Imagine. You’re working on a server emulator for game. And it’s not a bad game, but it’s so heavily client-sided, that developing a server for it is somewhat uninteresting. Even simple features, like NPC shops, might be client-sided, meaning you can’t ever have custom shops without client modificiations. Even simple interactions, like dropping an item, might have certain undesirable client-side checks, that make it so you can’t get rid of a subset of the game’s items. Client modification is always an option, of course. But as a server developer, it’s fun to push the limits of what you can do from the server, without touching the client. Not to mention, it’s always easier to not have to modify the client.
Last week I was asked a simple question on the Discord server for my Tree of Savior server emulator, Melia, in regards to reviving a feature that had been removed from the game, which lead me down a path that would remedy all of those problems for this particular project.
Did you see the packet to open player shops by any chance?
Well, I hadn’t, but I was kind of curious, so I dug around the client files a little, and I found the UI for personal shops. So far, so good. If the UI is still there, we could always open it, and be it via client modification. However, Tree of Savior has some interesting design aspects to it. Their UI system runs mostly on Lua, and there are a few packets that call into the Lua environment, like
ZC_ADDON_MSG, which the server can use to trigger events. That means you can control the UI to a certain degree, as long as events were set up by the scripts.
Unfortunately, there were no events for the personal shops, so the UI couldn’t be opened that way. But I remembered another packet. For some reason, some Lua functions appear to be called directly by the server, with a packet that contains a single string of Lua code, like this:
So I tried to call the function for opening the personal shop UI, and it worked. I just sent the function call, and the window opened. “Neat”, I thought. But the UI didn’t fully work. It didn’t react to my actions, so more work, and client modifications, would be required after all. I wanted to know what exactly was wrong through, so I debugged the script. As it turns out, the problem was simple. The UI elements had changed slightly after this feature had been removed, and one of the personal shop functions just failed to get a reference to one of the window’s elements. We’d only have to fix that one function, and the UI would be working again.
At this point, I started wondering about something. Note the parentheses in the snippet above. This packet,
ZC_EXEC_CLIENT_SCP, appeared to not just call a function by its name. It’s actual Lua code, a function call. If we could run arbitrary Lua code on the client via this packet… could we replace existing functions? See, Lua is a scripting language, and a flexible one. You can easily redefine functions, just by defining them again.
function Foo() print("foo") end function Foo() print("bar") end Foo(); -- prints "bar"
And every piece of Lua code that gets executed becomes part of the environment. If I were to instruct Lua to run another piece of code that contained a new
Foo function, that function would become the one that’s called going forth. A quick test confirmed my hopes. I fixed the function that was failing to find the UI element and sent it to the client. And it worked. Suddenly, the UI did its job again.
“Neat” wasn’t the right word for this anymore. You don’t usually modify a client’s behavior from the server to such a degree. If the entire UI is runing on Lua, and we’re able to freely modify it in any way we see fit, that would open up a lot of possibilities. It was slowly dawning on me how powerful this ability is. Care for another example?
After the UI was working again, I tried to actually create a personal shop, but nothing happened when I clicked the button. Looking at the client’s scripts again, this was the end of the rope. At that point, when you click the button, the scripts call into the client. There was nothing more that I could do. I launched a debugger and found where the internal function was failing, and I was able to skip a check to make it work and have the client send the shop creation packet, but this would not only require a modification of client data, but the client itself. Very unfortunate. However, ToS’s Lua scripts can do a few more things than just handle the UI, like sending chat messages.
Well, the UI, and subsequently the script, knows everything there is to know about the shop… the name, the items, the prices you specified… putting that into a string is not an issue. Sending that string via chat message, in the form of a command, is not an issue. And the server can read and interpret that command and act on it. Just like that, you open up a custom communication path, and you’re able to tell the server to create a shop.
Just being able to modify the UI is very useful. Being able to fix logic errors and customize behavior is amazing, but with the server instructing the client to run arbitrary Lua code and the client sending chat messages, you have two-way communication, with which you can do pretty much anything. That is mindblowing. What else could I do with this…
One of my bigger gripes about ToS, is that shops are client-sided. The client simply has a database with the shops and the items, and all the server does is tell the client which shop to open by name. A look at the shop UI scripts quickly showed, that it didn’t have to be that way. The script requests a list of shop items from the client, with a call like this:
But even though that’s a client function, it’s still in the Lua environment, so you can replace it.
function session.GetShopItemList() return MyCustomShopItems end
And the code you can have the client run isn’t limited to functions, you could also define global arrays, like say, a list of items that a shop should have. With these pieces falling into place, I was able to implement dynamic NPC shops in Melia. Not by modifying the client data, not even by modifying the data from the server via packet, which is technically possible as well, albeit with limitations, but by simply overwriting two client-side Lua functions and adding just a little bit of code to Melia.
To me, this discovery immediately made ToS ten times more interesting to work on. The UI is always a very limiting factor when working on server emulators. You typically have to live with what you get. Some games even hardcode their UIs and don’t even use any kind of markup, so that even if you’re okay with client modifications, it would be difficult to remove a button for some feature you’re not using on your server for example, or adding some kind of new element. And even if you can do that, then the behavior is usually defined in the client. By having access to the UI, the behavior, and a way to communicate with the server back and forth, there are very few limitations left.
It’s amazing how one simple packet can change your whole view on a project. Exciting times.