Ruin0x11 / OpenNefia

(Archived) Moddable engine reimplementation of the Japanese roguelike Elona.
MIT License
115 stars 18 forks source link

Consider moving parts of the core engine to a statically typed language #245

Open Ruin0x11 opened 3 years ago

Ruin0x11 commented 3 years ago

Related: #61.

Every time I program on top of ON I get some anxieties that stem from one fact: it's possible to mess up critical parts of the engine's data structures just by mistyping something in the REPL. Today, there is nothing preventing a mod from manually editing an InstancedMap's memory structure. And if that map gets saved, then a user with no programming knowledge has no way of undoing the damage except by restoring a backup.

But hold on. Dozens of popular games feature a Lua scripting engine, and none of them have any problems like this. Why is that?

The reason is that nearly all of those games chose to utilize an engine written in a compiled language with static types, and their Lua binding layers have runtime typechecking for userdata. You can't just index an unknown field in a struct with a static layout in memory. Writing your entire game in Lua means you have none of that. The only exception to "engine being written in a compiled language" that I can think of is ToME, but I don't think that ToME was built from the ground up to be moddable in the way ON was. As an outsider I can't really understand how T-Engine's API works just by looking at its ldoc pages. If ON is going to be explicitly referred to as "moddable," then it needs more stability guarantees.

But I have to ask: if the binding layers will typecheck anyway, why not do runtime typechecking in Lua? One reason would be that unless you have a robust OOP layer, you can't easily distinguish between different types of tables. Even Roblox maintains their own Lua dialect in order to add structural typechecking in a performant way (I would guess). And given that it's Lua, such typechecking can ultimately be defeated with enough metatable hacks. One of the benefits of compiled code is that you can't change it at runtime, or at all.

I was researching which language to use for the core, and was leaning on C# until I found their developer support on Linux is rather dismal. MonoDevelop, which is pretty much the only blessed way of developing Mono applications on Linux, silently dropped support for said OS and doesn't even mention this on their homepage. Why should I have to pay JetBrains $139 for a proprietary IDE just to manage C# projects on Linux?

Looking at MonoGame, it seems that nearly everything centered around asset management has to go through its content pipeline, and even projects like "Nopipeline" ultimately just run the pipeline for you in an automated way. This is terrible for iteration since you have to reboot the program every time you change an asset file, which you don't have to do in ON today. Beyond that, MonoGame's content pipeline is fundamentally incompatible with the way mods can contribute assets in ON in a dynamic fashion, and doesn't have CJK font support (while LÖVE has support out of the box). Still, what I don't really understand is how games like Stardew Valley have managed to use MonoGame successfully and support extensive mod systems. Why wouldn't ON with a MonoGame core be possible?

Because I can still stop working on the project in its experimental stages, there isn't much need for a partial rewrite. But if it becomes necessary to win back stability for the project at some point, then I would probably do something like this:

  1. Fork LÖVE and write a C API wrapper for it. In my opinion, the only real downside to using LÖVE for any new 2D game project is the fact that it has a hard dependency on Lua. Putting aside the choice of language, nearly everything needed to build ON was included in LÖVE from the start, without pointless roadblocks like MonoGame's content pipeline. If LÖVE was agnostic to any language, and it had bindings to something like C#, then I would probably find the experience superior to using the other engines for C# that I'm aware of.
  2. Bind this C library version of LÖVE to some other language. Which language to actually use is up in the air.
  3. Refactor the most critical parts of the core engine, like the object/ownership systems, to not be in Lua anymore.

The above would have to be finished before ON exits the "toy project" stage. The partial rewrite would ideally not change any code in existing mods; they would only use Lua. I feel that the API that ON uses is fairly intuitive, it's just the implementation that's easily prone to breakage. The choice of language wouldn't be used for easier modding necessarily, just a way to ensure that mods can't break important data structures that are guaranteed to never change after a certain point (like maps/characters/items). After this change, it would no longer be possible to add arbitrary properties to a map object; the ideal way of doing this would to attach a ModExtTable/capability list or something.

But honestly, with no users it's difficult to say if the complexity increase in maintaining a binding layer is worth the gains in stability. For now, ON will continue to use Lua for all its code.

Ruin0x11 commented 3 years ago

One thing is, if Teal were already production-ready and at the same level of stability as TypeScript when OpenNefia was first created, then the question of adding a binding layer to a different language would never have been asked. I would have adopted Teal instead.

When the solution to the problem of having no types is to create language bindings to a different language that has types, what would the actual end goal be? To move things like aspects into the typed language, so the schema is clear? Then what if a mod library is written in Lua exclusively instead of C#? The C# would have to call into Lua and the Lua would have to call into C#, making everything more complicated for a questionable amount of benefit.

At the point I decide that it should be possible to write aspects in Lua, because having to create an entire C# project just to add a single aspect with some trivial behavior is too inconvenient for modders, the battle has already been lost. There is no way to have a proper type system in stock Lua.

Any time I think about how data declaration would work if the engine were written entirely in C#, I just can't get behind a declarative data definition format like XML. I had tried it before in the past for this exact problem and it didn't really work out. There are some very useful features that are impractical to implement with an entirely declarative language - namely, you would lose the ability to write inline function callbacks without referencing an external file, which are used heavily in the current implementation of data definition. Every time you want some new external logic you have to add a special case translating from identifiers to the callback code for every single type of callback or behavior you want to support, and it's not clear how strings and these special identifiers should be distinguished from one another at a glance because, unlike inline function definitions, there is no such thing as a "function identifier" type in XML. Admittedly aspects do the same thing by moving a bunch of logic off into a different place, and I don't really like how they make the flow of the program harder to follow, but they're more acceptable in my mind because they're usually intended be reused with multiple item kinds and must be referenced by a require statement. (But that still doesn't help when all you want is to add some extra state to a single item in a one-off way. How aspects are finally going to turn out is still a major point of contention for me.)

It would make more sense to me to have a data system based on interfaces if C# were to be used exclusively instead. That would allow for implementing the optional callbacks in a cleaner way.