Rune Synergy Devblog 3

Originally I wanted the contributors to have an integrated experience, but writing an IDE is no laughing matter. On top of that a scripting language AND immediate user interface library. I may be crazy but I know when I’ve bitten more than I can chew. I haven’t given up either.

Key Terms & Jargon

Term Description
IDE An integrated development environment is a software application that combines code editing, debugging, and building tools to facilitate software development.
LSP A language server protocol protocol used between a code editor or IDE and a language server, which provides features like auto-completion, error checking, and code formatting.
ECS Entity Component System, A design pattern used in game development to separate the data and behavior of game objects, making it easier to manage complexity and optimize performance.
OSRS A popular online game, and the context for the items and objects mentioned in the blog post.
Yaegi A Go interpreter that allows you to use Go as a scripting language.
GOPATH An environment variable in Go that specifies the location of the Go workspace, where the source code, compiled binaries, and packages are stored.
code generation The process of automatically creating code based on templates or predefined patterns, usually to reduce repetitive tasks or improve code quality.
enum A data type consisting of a set of named values, used in programming languages to represent distinct elements of a collection.
script/glue.go A script that combines or connects different parts of a program, making it possible to use the same code in multiple places.

Editor integration, or not?

Go has a proper LSP, making it very accessible from nearly any editor/IDE of your liking. So my new goal became “How can I use Go as a scripting language?” Luckily there are already solutions written in Go, such as Yaegi: Another Elegant Go Interpreter. Plus they have a cool mascot.

It’s definitely proven to be an invaluable resource, however, it has some obstacles. You cannot pass generic functions or types to the symbols without instantiating the generic. Luckily this problem lead me to take a more generated code approach.

Additionally, if a symbol is referenced but not provided, Yaegi will attempt to evaluate it by searching your GOPATH or the provided GoPath option. This doesn’t play nice when you’re trying to define an instance of something that needs to be used by other scripts, since an entirely new instance would be created.

Code generation

Before you think “oh god you typed all that?” Nope. This is where code generation comes in. I start with a file called enum.stub.go which contains pretty much anything I need in my enum package.

In the root directory, “go generate ./…” triggers “cmd/gen-enum” which sucks up that stub and uses the template package and some hand-carved code. Only types beginning with an underscore will generate said code, everything else is emitted as is. This gives us our enum.go:

“go generate ./…” also triggers script/glue.go which just passes all the “static” packages to be generated by yaegi.

Finally, we have our symbols generated for each of those packages and they become accessible via script. One downside is that you need to “go generate ./…” every time a change is made to our stub. But now we can create, access, and update said structures on the fly!

Of course, this will change the way content will need to be scripted. Essentially there will be a global state that is allowed to be plugged into and changed.

Let’s say a script called “content/area/lumbridge/npcs.go” has a var which is supposed to reference an existing NPC:

Timeline .
Design Should it be the responsibility of the script to spawn and configure that NPC?
Design Should that NPC be defined as var guide *world.NPC and spawned in the scripts init()?
Runtime Then how will we handle that script reloading, despawning the old NPC, and spawning the new NPC?1

1 My first thought is to delete everything created by a given script before reloading it, that way any old entities or hooks disappear.

Although these requirements aren’t detrimental, I would still like to make scripting be as live as possible. A requirement such as restarting the entire server just isn’t an option.

The dream is not dead

So I won’t have my amazing entirely integrated editor, but on the bright side there are a ton of tools already available at my disposal and I get to use my favorite language using them.

This new code generation kick has also got me working on exporting all that JSON data I made back in Devblog 1 (Two months ago already.)

Here’s a rough example of what item configurations may look like:

For now the Model fields are strings, but I plan on doing some processing on the existing references I have to come up with some base names. It won’t be perfect but I’ll at least have a code reference to use.

I spent an entire day making sure that generated items are grouped together properly, so you’ll find all the metal axes under axe.go for example. An added benefit of this is they get to share specific values which help reduce duplicate data (Which you can see in the screenshot above with the coins for example.)

There are still a lot of items that need to be properly named and grouped so it’s a work in progress:

You may have already arrived at this conclusion- but it makes it much easier to search by reference and go directly to an item configuration; since it’s within the same codebase. This also allows for things like autocomplete to function which is great.

I thought about extending the data, I didn’t want to just fill the structs up with more fields that would only be used by a few items. The solution is similar to ECS except there isn’t a system (maybe storage.) Here’s some code accessing all the axes with enum.Axes().

It would have been nice to bring the Axe closer, like using a generic method (i Item) Set[T any](t T) Item and just chaining a bunch of Set()s. The problem is I can’t use generics. The alternate solution would be to generate SetAxe, SetFood which is a possibility, but I’ll have to prototype. I don’t want to muck up the symbols of Item with setters.

Now I just need to generate the code for Item to Axe as seen on the bottom right on line 20: axe, ok := item.Axe(). Keep in mind I say generate because this will probably be used a lot for all sorts of content.

So for example they may need to know if an item is a WaterContainer or an object is a WaterSource. The water container can reference the next item it becomes when you add to or remove from it.

Food, Bones, Gem, Craftable, Tannable, Pickaxe… come to think of it I probably could have sacrificed the Equipment and Weapon fields in Item for composite types instead…

I’m getting ahead of myself.

Client prototype restructure

I introduced a simple resource package to load assets. The demo is still up. There’s still no gameplay but I’m definitely downloading lots of stuff and loading them onto the GPU.

I did some spring cleaning and reorganized the packaging for the prototype client:

Before

After

It’s nothing too crazy, I just simplified a lot by flattening the packages and joining platform-specific code into the app, input and internal/gl packages. I’ll probably destroy the ui package since it fell out of scope.

What’s next?

My next short-term goal is to get some proper model names for our items and also get the last bit of item names/groups fixed up. It takes time since I’m literally going item to item and naming them potentially based on their usage. I can either quickly decide just by reading the examine, or go to the OSRS wiki and figure out where it’s used.

Once that’s complete, I’ll move to NPCs, and then Objects. Eventually, I’ll need to solve proper ID tracking since the current solution omits IDs entirely. That’s not a big deal at all, just another step in the process.

Then… Get back to the client work I suppose! We’ll see.